[Fixes #8066] Added native ENS registration

Signed-off-by: Andrey Shovkoplyas <motor4ik@gmail.com>
This commit is contained in:
Julien Eluard 2019-06-03 09:42:29 +02:00 committed by Andrey Shovkoplyas
parent 23702dc1e7
commit 8aa16520c6
No known key found for this signature in database
GPG Key ID: EAAB7C8622D860A4
21 changed files with 882 additions and 31 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -246,3 +246,5 @@
(def v23 (assoc-in v22
[:properties :keycard-key-uid]
{:type :string :optional true}))
(def v24 (update v23 :properties merge {:usernames {:type "string[]" :optional true}}))

View File

@ -113,6 +113,11 @@
extension/v12
account/v23])
(def v29 [network/v1
bootnode/v4
extension/v12
account/v24])
;; put schemas ordered by version
(def schemas [{:schema v1
:schemaVersion 1
@ -197,4 +202,7 @@
:migration (constantly nil)}
{:schema v28
:schemaVersion 28
:migration (constantly nil)}
{:schema v29
:schemaVersion 29
:migration (constantly nil)}])

150
src/status_im/ens/core.cljs Normal file
View File

@ -0,0 +1,150 @@
(ns status-im.ens.core
(:require [clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.accounts.update.core :as accounts.update]
[status-im.ethereum.abi-spec :as abi-spec]
[status-im.ethereum.contracts :as contracts]
[status-im.ethereum.core :as ethereum]
[status-im.ethereum.ens :as ens]
[status-im.ethereum.resolver :as resolver]
[status-im.ethereum.stateofus :as stateofus]
[status-im.ui.screens.navigation :as navigation]
[status-im.utils.fx :as fx]
[status-im.utils.money :as money]
[status-im.signing.core :as signing])
(:refer-clojure :exclude [name]))
(defn fullname [custom-domain? username]
(if custom-domain?
username
(stateofus/subdomain username)))
(re-frame/reg-fx
:ens/resolve-address
(fn [[registry name cb]]
(ens/get-addr registry name cb)))
(re-frame/reg-fx
:ens/resolve-pubkey
(fn [[registry name cb]]
(resolver/pubkey registry name cb)))
(defn assoc-state-for [db username state]
(assoc-in db [:ens/registration :states username] state))
(defn assoc-details-for [db username k v]
(assoc-in db [:ens/names :details username k] v))
(defn assoc-username-candidate [db username]
(assoc-in db [:ens/registration :username-candidate] username))
(defn empty-username-candidate [db] (assoc-username-candidate db ""))
(fx/defn set-state
{:events [:ens/set-state]}
[{:keys [db]} username state]
{:db (assoc-state-for db username state)})
(defn- on-resolve [registry custom-domain? username address public-key s]
(cond
(= (ethereum/normalized-address address) (ethereum/normalized-address s))
(resolver/pubkey registry (fullname custom-domain? username)
#(re-frame/dispatch [:ens/set-state username (if (= % public-key) :connected :owned)]))
(and (nil? s) (not custom-domain?)) ;; No address for a stateofus subdomain: it can be registered
(re-frame/dispatch [:ens/set-state username :registrable])
:else
(re-frame/dispatch [:ens/set-state username :unregistrable])))
(fx/defn register-name
[{:keys [db] :as cofx} contract custom-domain? username address public-key]
(let [{:keys [x y]} (ethereum/coordinates public-key)]
(signing/eth-transaction-call
cofx
{:contract (contracts/get-address db :status/snt)
:method "approveAndCall(address,uint256,bytes)"
:params [contract
(money/unit->token 10 18)
(abi-spec/encode "register(bytes32,address,bytes32,bytes32)"
[(ethereum/sha3 username) address x y])]
:on-result [:ens/save-username custom-domain? username]
:on-error [:ens/on-registration-failure]})))
(defn- valid-custom-domain? [username]
(and (ens/is-valid-eth-name? username)
(stateofus/lower-case? username)))
(defn- valid-username? [custom-domain? username]
(if custom-domain?
(valid-custom-domain? username)
(stateofus/valid-username? username)))
(defn- state [custom-domain? username]
(cond
(string/blank? username) :initial
(> 4 (count username)) :too-short
(valid-username? custom-domain? username) :valid
:else :invalid))
(fx/defn set-username-candidate
{:events [:ens/set-username-candidate]}
[{:keys [db]} custom-domain? username]
(let [state (state custom-domain? username)
valid? (valid-username? custom-domain? username)
name (fullname custom-domain? username)]
(merge
{:db (-> db
(assoc-username-candidate username)
(assoc-state-for username state))}
(when (and name (= :valid state))
(let [{:keys [account/account]} db
{:keys [address public-key]} account
registry (get ens/ens-registries (ethereum/chain-keyword db))]
{:ens/resolve-address [registry name #(on-resolve registry custom-domain? username address public-key %)]})))))
(fx/defn clear-cache-and-navigate-back
{:events [:ens/clear-cache-and-navigate-back]}
[{:keys [db] :as cofx} _]
(fx/merge cofx
{:db (assoc db :ens/registration nil)} ;; Clear cache
(navigation/navigate-back)))
(fx/defn switch-domain-type
{:events [:ens/switch-domain-type]}
[{:keys [db]} _]
{:db (-> (update-in db [:ens/registration :custom-domain?] not)
(empty-username-candidate))})
(fx/defn save-username
{:events [:ens/save-username]}
[{:keys [db] :as cofx} custom-domain? username]
(let [name (fullname custom-domain? username)
db (update-in db [:account/account :usernames] #((fnil conj []) %1 %2) name)]
(accounts.update/account-update cofx
{:usernames (get-in db [:account/account :usernames])}
{:success-event [:ens/set-state username :saved]})))
(fx/defn on-registration-failure
{:events [:ens/on-registration-failure]}
[{:keys [db]} username]
{:db (assoc-state-for db username :registration-failed)})
(fx/defn register
{:events [:ens/register]}
[cofx {:keys [contract custom-domain? username address public-key]}]
(register-name cofx contract custom-domain? username address public-key))
(fx/defn store-name-detail
{:events [:ens/store-name-detail]}
[{:keys [db]} name k v]
{:db (assoc-details-for db name k v)})
(fx/defn navigate-to-name
{:events [:ens/navigate-to-name]}
[{:keys [db] :as cofx} name]
(let [registry (get ens/ens-registries (ethereum/chain-keyword db))]
(fx/merge cofx
{:ens/resolve-address [registry name #(re-frame/dispatch [:ens/store-name-detail name :address %])]
:ens/resolve-pubkey [registry name #(re-frame/dispatch [:ens/store-name-detail name :public-key %])]}
(navigation/navigate-to-cofx :ens-name-details name))))

View File

@ -65,6 +65,14 @@
(when s
(string/replace s hex-prefix "")))
(def ^:const public-key-length 128)
(defn coordinates [public-key]
(when-let [hex (naked-address public-key)]
(when (= public-key-length (count (subs hex 2)))
{:x (normalized-address (subs hex 1 65))
:y (normalized-address (subs hex 66))})))
(defn address? [s]
(when s
(.isAddress (utils) s)))

View File

@ -41,8 +41,7 @@
:outputs ["address"]
:on-success
(fn [[address]]
(when-not (= address default-address)
(cb address)))}))
(cb (when-not (= address default-address) address)))}))
(defn owner
[registry ens-name cb]

View File

@ -0,0 +1,24 @@
(ns status-im.ethereum.stateofus
(:require [clojure.string :as string]))
(def domain "stateofus.eth")
(defn subdomain [username]
(str username "." domain))
(defn username [name]
(when (string/ends-with? name domain)
(first (string/split name "."))))
(def registrars
{:mainnet "0xDB5ac1a559b02E12F29fC0eC0e37Be8E046DEF49"
:testnet "0x11d9F481effd20D76cEE832559bd9Aca25405841"})
(defn lower-case? [s]
(when s
(= s (string/lower-case s))))
(defn valid-username? [username]
(boolean
(and (lower-case? username)
(re-find #"^[a-z0-9]+$" username))))

View File

@ -1,5 +1,6 @@
(ns status-im.events
(:require [re-frame.core :as re-frame]
(:require [clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.accounts.core :as accounts]
[status-im.accounts.create.core :as accounts.create]
[status-im.accounts.login.core :as accounts.login]
@ -21,6 +22,8 @@
[status-im.contact-recovery.core :as contact-recovery]
[status-im.contact.block :as contact.block]
[status-im.contact.core :as contact]
[status-im.ethereum.core :as ethereum]
[status-im.ethereum.ens :as ethereum.ens]
[status-im.ethereum.subscriptions :as ethereum.subscriptions]
[status-im.ethereum.transactions.core :as ethereum.transactions]
[status-im.fleet.core :as fleet]
@ -1548,11 +1551,6 @@
;; profile module
(handlers/register-handler-fx
:profile.ui/ens-names-button-pressed
(fn [cofx]
(browser/open-url cofx "names.statusnet.eth")))
(handlers/register-handler-fx
:profile.ui/keycard-settings-button-pressed
(fn [cofx]
@ -1997,4 +1995,4 @@
(re-frame/reg-fx
:dismiss-keyboard
(fn []
(react/dismiss-keyboard!)))
(react/dismiss-keyboard!)))

View File

@ -16,7 +16,8 @@
:warning-sign (js-require/js-require "./resources/images/ui/warning-sign.png")
:phone-nfc-on (js-require/js-require "./resources/images/ui/phone-nfc-on.png")
:phone-nfc-off (js-require/js-require "./resources/images/ui/phone-nfc-off.png")
:dapp-store (js-require/js-require "./resources/images/ui/dapp-store.png")})
:dapp-store (js-require/js-require "./resources/images/ui/dapp-store.png")
:ens-header (js-require/js-require "./resources/images/ui/ens-header.png")})
(def loaded-images (atom {}))

View File

@ -12,10 +12,11 @@
[status-im.chat.models :as chat.models]
[status-im.constants :as constants]
[status-im.contact.db :as contact.db]
[status-im.ethereum.core :as ethereum]
[status-im.ethereum.stateofus :as stateofus]
[status-im.ethereum.tokens :as tokens]
[status-im.ethereum.transactions.core :as transactions]
[status-im.ethereum.transactions.etherscan :as transactions.etherscan]
[status-im.ethereum.core :as ethereum]
[status-im.fleet.core :as fleet]
[status-im.group-chats.db :as group-chats.db]
[status-im.i18n :as i18n]
@ -34,6 +35,7 @@
[status-im.utils.build :as build]
[status-im.utils.config :as config]
[status-im.utils.datetime :as datetime]
[status-im.utils.hex :as utils.hex]
[status-im.utils.identicon :as identicon]
[status-im.utils.money :as money]
[status-im.utils.platform :as platform]
@ -165,6 +167,10 @@
;;ethereum
(reg-root-key-sub :ethereum/current-block :ethereum/current-block)
;;ens
(reg-root-key-sub :ens/registration :ens/registration)
(reg-root-key-sub :ens/names :ens/names)
;;signing
(reg-root-key-sub :signing/tx :signing/tx)
(reg-root-key-sub :signing/sign :signing/sign)
@ -1787,6 +1793,44 @@
(#{"0" "0.0" "0.00"} screen-snt-amount)
(string/ends-with? screen-snt-amount ".")))))))))
;;ENS ==================================================================================================================
(re-frame/reg-sub
:ens.stateofus/registrar
:<- [:account/network]
(fn [network]
(let [chain (ethereum/network->chain-keyword network)]
(get stateofus/registrars chain))))
(re-frame/reg-sub
:account/usernames
:<- [:account/account]
(fn [account]
(:usernames account)))
(re-frame/reg-sub
:ens.registration/screen
:<- [:ens/registration]
:<- [:ens.stateofus/registrar]
:<- [:account/account]
(fn [[{:keys [custom-domain? username-candidate] :as ens} registrar {:keys [address public-key]}] _]
{:state (get-in ens [:states username-candidate])
:username username-candidate
:custom-domain? (or custom-domain? false)
:contract registrar
:address address
:public-key public-key}))
(re-frame/reg-sub
:ens.name/screen
:<- [:get-screen-params :ens-name-details]
:<- [:ens/names]
(fn [[name ens] _]
(let [{:keys [address public-key]} (get-in ens [:details name])]
{:name name
:address address
:public-key public-key})))
;;SIGNING =============================================================================================================
(re-frame/reg-sub

View File

@ -75,14 +75,13 @@
(def settings-item-separator
{:margin-left 16})
(defn settings-item
[large?]
{:padding-left 16
:padding-right 8
:flex 1
:flex-direction :row
:align-items :center
:height (if large? 82 52)})
(def settings-item
{:padding-left 16
:padding-right 8
:flex 1
:flex-direction :row
:align-items :center
:height 64})
(defn settings-item-icon
[icon-color large?]
@ -111,7 +110,6 @@
(def settings-item-main-text-container
{:flex-direction :row
:height 18
:align-items :center})
(def settings-item-subtext

View File

@ -114,7 +114,7 @@
:icon-opts {:color colors/white}}])
(defn big-list-item
[{:keys [style text text-color subtext value action-fn active? destructive? hide-chevron?
[{:keys [style text text-color text-style subtext value action-fn active? destructive? hide-chevron?
accessory-value text-color new? activity-indicator
accessibility-label icon icon-color image-source icon-content]
:or {icon-color colors/blue
@ -123,14 +123,14 @@
active? true
style {}}}]
{:pre [(or icon image-source activity-indicator)
(and action-fn text)
text
(or (nil? accessibility-label) (keyword? accessibility-label))]}
[react/touchable-highlight
{:on-press action-fn
:style style
:accessibility-label accessibility-label
:disabled (not active?)}
[react/view (styles/settings-item subtext)
[react/view styles/settings-item
(cond
icon
[react/view (styles/settings-item-icon icon-color subtext)
@ -148,14 +148,14 @@
[react/view {:style styles/new-label}
[react/text {:style styles/new-label-text}
(string/upper-case (i18n/label :t/new))]])
[react/text {:style (styles/settings-item-text text-color)}
[react/text {:style (merge (styles/settings-item-text text-color) text-style)}
text]]
[react/view {:style {:margin-top 2
:justify-content :flex-start}}
[react/text {:style styles/settings-item-subtext
:number-of-lines 2}
subtext]]]
[react/text {:style (styles/settings-item-text text-color)
[react/text {:style (merge (styles/settings-item-text text-color) text-style)
:number-of-lines 1}
text])
(when accessory-value

View File

@ -0,0 +1,521 @@
(ns status-im.ui.screens.ens.views
"
+-------------+
| Initial |
+-----+-------+
|
| Typing
|
v
+--------------+ +----------------+
| Valid | | Invalid/reason |
+------+-------+ +-------+--------+
| |
+----------+----------+
|
| Checking
|
|
v
+------------------------------------------+
| +--------------+ +----------------+ |
| | Unregistrable| | Registrable | | +-----------------------------------+ +-------------+
| +--------------+ +----------------+ | | Connected/details | | Not owned |
| | | (none, address, public+key, all) | +-------------+
| | +----------+------------------------+
| Name available | |
+-------------------+----------------------+ |
| |
| |
| |
| Registering | Connecting
| (on-chain, 1 tx) | (on-chain, 1tx per info to connect)
| |
+-----------------------+------------------+
|
|
| Saving
|
|
+-------+-----+
| Saved |
+-------------+
"
(:require [re-frame.core :as re-frame]
[reagent.core :as reagent]
[status-im.ens.core :as ens]
[status-im.ethereum.core :as ethereum]
[status-im.ethereum.ens :as ethereum.ens]
[status-im.ethereum.stateofus :as stateofus]
[status-im.i18n :as i18n]
[status-im.react-native.resources :as resources]
[status-im.ui.components.checkbox.view :as checkbox]
[status-im.ui.components.colors :as colors]
[status-im.ui.components.common.common :as components.common]
[status-im.ui.components.icons.vector-icons :as vector-icons]
[status-im.ui.components.list.views :as list]
[status-im.ui.components.react :as react]
[status-im.ui.components.status-bar.view :as status-bar]
[status-im.ui.components.toolbar.actions :as actions]
[status-im.ui.components.toolbar.view :as toolbar])
(:require-macros [status-im.utils.views :as views]))
;; Components
(defn- button [{:keys [on-press] :as m} label]
[components.common/button (merge {:button-style {:margin-vertical 8
:padding-horizontal 32
:justify-content :center
:align-items :center}
:on-press on-press
:label label}
m)])
(defn- link [{:keys [on-press]} label]
[react/touchable-opacity {:on-press on-press :style {:justify-content :center}}
[react/text {:style {:color colors/blue}}
label]])
(defn- section [{:keys [title content]}]
[react/view {:style {:margin-horizontal 16 :align-items :flex-start}}
[react/text {:style {:color colors/gray :font-size 15}}
title]
[react/view {:margin-top 8 :padding-horizontal 16 :padding-vertical 12 :border-width 1 :border-radius 12
:border-color colors/gray-lighter}
[react/text {:style {:font-size 15}}
content]]])
;; Name details
(views/defview name-details []
(views/letsubs [{:keys [name address public-key]} [:ens.name/screen]]
(let [pending? (nil? address)]
[react/view {:style {:flex 1}}
[status-bar/status-bar {:type :main}]
[toolbar/simple-toolbar
name]
[react/scroll-view {:style {:flex 1}}
[react/view {:style {:flex 1 :margin-horizontal 16}}
[react/view {:flex-direction :row :align-items :center :margin-top 20}
[react/view {:style {:margin-right 16}}
[components.common/logo
{:size 40
:icon-size 16}]]
[react/text {:style {:typography :title}}
(if pending?
(i18n/label :t/ens-transaction-pending)
(str (i18n/label :t/ens-10-SNT) ", deposit unlocked"))]]]
[react/view {:style {:margin-top 22}}
(when-not pending?
[section {:title (i18n/label :t/ens-wallet-address)
:content (ethereum/normalized-address address)}])
(when-not pending?
[react/view {:style {:margin-top 14}}
[section {:title (i18n/label :t/key)
:content public-key}]])
[react/view {:style {:margin-top 16 :margin-bottom 32}}
[list/big-list-item {:text (i18n/label :t/ens-remove-username)
:subtext (i18n/label :t/ens-remove-hints)
:text-color colors/gray
:text-style {:font-weight "500"}
:icon :main-icons/close
:icon-color colors/gray
:hide-chevron? true}]
[react/view {:style {:margin-top 10}}
[list/big-list-item {:text (i18n/label :t/ens-release-username)
:text-color colors/gray
:text-style {:font-weight "500"}
:subtext (i18n/label :t/ens-locked)
:action-fn #(re-frame/dispatch [:navigate-to :ens-register])
:icon :main-icons/delete
:icon-color colors/gray
:active? false
:hide-chevron? true}]]]]]])))
;; Terms
(defn- term-point [content]
[react/view {:style {:flex 1 :margin-top 24 :margin-horizontal 16 :flex-direction :row}}
[react/view {:style {:width 16 :margin-top 8}}
[react/view {:style {:background-color colors/gray :width 4 :height 4 :border-radius 25}}]]
[react/text {:style {:flex 1 :font-size 15}}
content]])
(defn- etherscan-url [address]
(str "https://etherscan.io/address/" address))
(views/defview terms []
(views/letsubs [{:keys [contract]} [:get-screen-params :ens-terms]]
[react/scroll-view {:style {:flex 1}}
[status-bar/status-bar {:type :main}]
[toolbar/simple-toolbar
(i18n/label :t/ens-terms-registration)]
[react/view {:style {:height 136 :background-color colors/gray-lighter :justify-content :center :align-items :center}}
[react/text {:style {:text-align :center :typography :header :letter-spacing -0.275}}
(i18n/label :t/ens-terms-header)]]
[react/view
[term-point
(i18n/label :t/ens-terms-point-1)]
[term-point
(i18n/label :t/ens-terms-point-2)]
[term-point
(i18n/label :t/ens-terms-point-3)]
[term-point
(i18n/label :t/ens-terms-point-4)]
[term-point
(i18n/label :t/ens-terms-point-5)]
[term-point
(i18n/label :t/ens-terms-point-6)]
[term-point
(i18n/label :t/ens-terms-point-7)]]
[react/view
[react/text {:style {:font-size 15 :margin-top 24 :margin-horizontal 16 :font-weight "700"}}
(i18n/label :t/ens-terms-point-8)]
[term-point
(i18n/label :t/ens-terms-point-9 {:address contract})]
[react/view {:style {:align-items :center :margin-top 16 :margin-bottom 8}}
[link {:on-press #(.openURL (react/linking) (etherscan-url contract))}
(i18n/label :t/etherscan-lookup)]]
[term-point
(i18n/label :t/ens-terms-point-10)]
[react/view {:style {:align-items :center :margin-top 16 :margin-bottom 8}}
[link {:on-press #(.openURL (react/linking) (etherscan-url (:mainnet ethereum.ens/ens-registries)))}
(i18n/label :t/etherscan-lookup)]]]]))
;; Registration
(defn- valid-domain? [state]
(#{:registrable :owned :connected} state))
(defn- final-state? [state]
(#{:saved :registered :registration-failed} state))
(defn- main-icon [state]
(cond
(valid-domain? state) :main-icons/check
(= state :unregistrable) :main-icons/cancel
:else :main-icons/username))
(defn- icon-wrapper [color icon]
[react/view {:style {:margin-right 10 :width 32 :height 32 :border-radius 25
:align-items :center :justify-content :center :background-color color}}
icon])
(defn- input-action [{:keys [state custom-domain? username]}]
(if (= :connected state)
;; Already registered, just need to save the contact
[:ens/save-username custom-domain? username]
[:ens/set-state username :registering]))
(defn- disabled-input-action []
[icon-wrapper colors/gray
[vector-icons/icon :main-icons/arrow-right {:color colors/white}]])
(defn- input-icon [{:keys [state custom-domain? username] :as props} usernames]
(cond
(= state :registering)
nil
(= state :valid)
[icon-wrapper colors/blue
[react/activity-indicator {:color colors/white}]]
(valid-domain? state)
(let [name (ens/fullname custom-domain? username)]
(if (contains? (set usernames) name)
[disabled-input-action]
[react/touchable-highlight {:on-press #(re-frame/dispatch (input-action props))}
[icon-wrapper colors/blue
[vector-icons/icon :main-icons/arrow-right {:color colors/white}]]]))
:else
[disabled-input-action]))
(defn- default-name [custom-domain?]
(if custom-domain?
"vitalik94.domain.eth"
"vitalik94"))
(defn- domain-label [custom-domain?]
(if custom-domain?
(i18n/label :t/ens-custom-domain)
(str "." stateofus/domain)))
(defn- domain-switch-label [custom-domain?]
(if custom-domain?
(i18n/label :t/ens-want-domain)
(i18n/label :t/ens-want-custom-domain)))
(defn- help-message [state custom-domain?]
(case state
(:initial :too-short)
(if custom-domain?
(i18n/label :t/ens-custom-username-hints)
(i18n/label :t/ens-username-hints))
:invalid
(if custom-domain?
(i18n/label :t/ens-custom-username-hints)
(i18n/label :t/ens-username-invalid))
:unregistrable
(if custom-domain?
(i18n/label :t/ens-custom-username-unregistrable)
(i18n/label :t/ens-username-unregistrable))
:registrable
(i18n/label :t/ens-username-registrable)
:owned
(i18n/label :t/ens-username-owned)
:connected
(i18n/label :t/ens-username-connected)
""))
(defn- on-username-change [custom-domain? username]
(re-frame/dispatch [:ens/set-username-candidate custom-domain? username]))
(defn- on-registration [props]
(re-frame/dispatch [:ens/register props]))
(defn- agreement [{:keys [checked contract]}]
[react/view {:flex-direction :row :margin-horizontal 20 :margin-top 14 :align-items :flex-start :justify-content :center}
[checkbox/checkbox {:checked? @checked
:style {:padding 0}
:on-value-change #(reset! checked %)}]
[react/view {:style {:padding-left 10}}
[react/view {:style {:flex-direction :row}}
[react/text
(i18n/label :t/ens-agree-to)]
[link {:on-press #(re-frame/dispatch [:navigate-to :ens-terms {:contract contract}])}
(i18n/label :t/ens-terms-registration)]]
[react/text
(i18n/label :t/ens-understand)]]])
(defn- registration-bottom-bar [{:keys [checked] :as props}]
[react/view {:style {:height 60
:background-color colors/white
:border-top-width 1
:border-top-color colors/gray-lighter}}
[react/view {:style {:margin-horizontal 16
:flex-direction :row
:justify-content :space-between}}
[react/view {:flex-direction :row}
[react/view {:style {:margin-top 12 :margin-right 8}}
[components.common/logo
{:size 36
:icon-size 16}]]
[react/view {:flex-direction :column :margin-vertical 8}
[react/text {:style {:font-size 15}}
(i18n/label :t/ens-10-SNT)]
[react/text {:style {:color colors/gray :font-size 15}}
(i18n/label :t/ens-deposit)]]]
[button {:disabled? (not @checked)
:label-style (when (not @checked) {:color colors/gray})
:on-press #(on-registration props)}
(i18n/label :t/ens-register)]]])
(defn- registration [{:keys [address public-key] :as props}]
[react/view {:style {:flex 1 :margin-top 24}}
[section {:title (i18n/label :t/ens-wallet-address)
:content address}]
[react/view {:style {:margin-top 14}}
[section {:title (i18n/label :t/key)
:content public-key}]]
[agreement props]])
(defn- icon [{:keys [state]}]
[react/view {:style {:margin-top 68 :margin-bottom 24 :width 60 :height 60 :border-radius 30
:background-color colors/blue :align-items :center :justify-content :center}}
[vector-icons/icon (main-icon state) {:color colors/white}]])
(defn- username-input [{:keys [custom-domain? username state] :as props} usernames]
[react/view {:flex-direction :row :justify-content :center}
[react/text-input {:on-change-text #(on-username-change custom-domain? %)
:on-submit-editing #(on-registration props)
:auto-capitalize :none
:auto-correct false
:default-value username
:auto-focus true
:text-align :center
:placeholder (default-name custom-domain?)
:style {:flex 1 :font-size 22
(if (= state :registering) :padding-horizontal :padding-left) 48}}]
[input-icon props usernames]])
(defn- final-state-label [state]
(case state
:registered
(i18n/label :t/ens-registered-title)
:saved
(i18n/label :t/ens-saved-title)
:registration-failed
(i18n/label :t/ens-registration-failed-title)
""))
(defn- final-state-details [{:keys [state username]}]
(case state
:registered
[react/text {:style {:color colors/gray :font-size 14}}
(i18n/label :t/ens-registered)]
:registration-failed
[react/text {:style {:color colors/gray :font-size 14}}
(i18n/label :t/ens-registration-failed)]
:saved
[react/view {:style {:flex-direction :row :align-items :center}}
[react/nested-text
{:style {}}
(stateofus/subdomain username)
[{:style {:color colors/gray}}
(i18n/label :t/ens-saved)]]]
[react/view {:flex-direction :row :margin-left 6 :margin-top 14 :align-items :center}
[react/text
(str (i18n/label :t/ens-terms-registration) " ->")]]))
(defn- finalized-icon [{:keys [state]}]
(case state
:registration-failed
[react/view {:style {:width 40 :height 40 :border-radius 30 :background-color colors/red-light
:align-items :center :justify-content :center}}
[vector-icons/icon :main-icons/warning {:color colors/red}]]
[react/view {:style {:width 40 :height 40 :border-radius 30 :background-color colors/gray-lighter
:align-items :center :justify-content :center}}
[vector-icons/icon :main-icons/check {:color colors/blue}]]))
(defn- registration-finalized [{:keys [state username] :as props}]
[react/view {:style {:flex 1 :align-items :center :justify-content :center}}
[finalized-icon props]
[react/text {:style {:typography :header :margin-top 32 :margin-horizontal 32 :text-align :center}}
(final-state-label state)]
[react/view {:align-items :center :margin-horizontal 32 :margin-top 12 :margin-bottom 20 :justify-content :center}
[final-state-details props]]
(if (= state :registration-failed)
[react/view
[button {:on-press #(re-frame/dispatch [:ens/set-state username :registering])}
(i18n/label :t/retry)]
[button {:background? false
:on-press #(re-frame/dispatch [:ens/clear-cache-and-navigate-back])}
(i18n/label :t/cancel)]]
[button {:on-press #(re-frame/dispatch [:ens/clear-cache-and-navigate-back])}
(i18n/label :t/ens-got-it)])])
(views/defview registration-pending [{:keys [state custom-domain?] :as props} usernames]
(views/letsubs [usernames [:account/usernames]]
[react/view {:style {:flex 1}}
[react/scroll-view {:style {:flex 1}}
[react/view {:style {:flex 1}}
[react/view {:style {:flex 1 :align-items :center :justify-content :center}}
[icon props]
[username-input props usernames]
[react/view {:style {:height 36 :align-items :center :justify-content :space-between :padding-horizontal 12
:margin-top 24 :margin-horizontal 16 :border-color colors/gray-lighter :border-radius 20
:border-width 1 :flex-direction :row}}
[react/text {:style {:font-size 12 :typography :main-medium}}
(domain-label custom-domain?)]
[react/view {:flex 1 :min-width 24}]
(when-not (= state :registering)
;; Domain type is not shown during registration
[react/touchable-highlight {:on-press #(re-frame/dispatch [:ens/switch-domain-type])}
[react/text {:style {:color colors/blue :font-size 12 :typography :main-medium} :number-of-lines 2}
(domain-switch-label custom-domain?)]])]]
(if (= state :registering)
[registration props]
[react/text {:style {:flex 1 :margin-top 16 :margin-horizontal 16 :font-size 14 :text-align :center}}
(help-message state custom-domain?)])]]
(when (= state :registering)
[registration-bottom-bar props])]))
(defn- toolbar []
[toolbar/toolbar nil
[toolbar/nav-button (actions/back #(re-frame/dispatch [:ens/clear-cache-and-navigate-back]))]
[toolbar/content-title (i18n/label :t/ens-your-username)]])
(views/defview register []
(views/letsubs [{:keys [address state] :as props} [:ens.registration/screen]]
(let [checked (reagent/atom false)
props (merge props {:checked checked :address (ethereum/normalized-address address)})]
[react/keyboard-avoiding-view {:flex 1}
[status-bar/status-bar {:type :main}]
[toolbar]
(if (final-state? state)
[registration-finalized props]
[registration-pending props])])))
;; Welcome
(defn- welcome-item [{:keys [icon-label title]} content]
[react/view {:style {:flex 1 :margin-top 24 :margin-left 16 :flex-direction :row}}
[react/view {:style {:height 40 :width 40 :border-radius 25 :border-width 1 :border-color colors/gray-lighter
:align-items :center :justify-content :center}}
[react/text {:style {:typography :title}}
icon-label]]
[react/view {:style {:flex 1 :margin-horizontal 16}}
[react/text {:style {:font-size 15 :typography :main-semibold}}
title]
content]])
(defn- welcome []
[react/view {:style {:flex 1}}
[react/scroll-view {:content-container-style {:align-items :center}}
[react/image {:source (:ens-header resources/ui)
:style {:margin-top 32}}]
[react/text {:style {:margin-top 32 :margin-bottom 8 :typography :header}}
(i18n/label :t/ens-get-name)]
[react/text {:style {:margin-top 8 :margin-bottom 24 :color colors/gray :font-size 15 :margin-horizontal 16
:text-align :center}}
(i18n/label :t/ens-welcome-hints)]
[welcome-item {:icon-label "1" :title (i18n/label :t/ens-welcome-point-1-title)}
[react/view {:flex-direction :row}
[react/nested-text
{:style {:color colors/gray}}
(i18n/label :t/ens-welcome-point-1)
[{:style {:text-decoration-line :underline :color colors/black}}
(stateofus/subdomain "myname")]]]]
[welcome-item {:icon-label "2" :title (i18n/label :t/ens-welcome-point-2-title)}
[react/text {:style {:color colors/gray}}
(i18n/label :t/ens-welcome-point-2)]]
[welcome-item {:icon-label "3" :title (i18n/label :t/ens-welcome-point-3-title)}
[react/text {:style {:color colors/gray}}
(i18n/label :t/ens-welcome-point-3)]]
[welcome-item {:icon-label "@" :title (i18n/label :t/ens-welcome-point-4-title)}
[react/text {:style {:color colors/gray}}
(i18n/label :t/ens-welcome-point-4)]]
[react/text {:style {:margin-top 16 :text-align :center :color colors/gray :typography :caption :padding-bottom 96}}
(i18n/label :t/ens-powered-by)]]
[react/view {:align-items :center :background-color colors/white
:position :absolute :left 0 :right 0 :bottom 0
:border-top-width 1 :border-top-color colors/gray-lighter}
[button {:on-press #(re-frame/dispatch [:navigate-to :ens-register])
:label (i18n/label :t/get-started)}]]])
(defn- registered [names]
[react/scroll-view {:style {:flex 1}}
[react/view {:style {:flex 1 :margin-top 8}}
[list/big-list-item {:text (i18n/label :t/ens-add-username)
:action-fn #(re-frame/dispatch [:navigate-to :ens-register])
:icon :main-icons/add}]]
[react/view {:style {:margin-top 22}}
[react/text {:style {:color colors/gray :margin-horizontal 16}}
(i18n/label :t/ens-your-usernames)]
(if (seq names)
[react/view {:style {:margin-top 8}}
(for [name names]
^{:key name}
[react/view
(let [stateofus-username (stateofus/username name)
s (or stateofus-username name)]
[list/big-list-item {:text s
:subtext (when stateofus-username stateofus/domain)
:action-fn #(re-frame/dispatch [:ens/navigate-to-name name])
:icon :main-icons/username}])])]
[react/text {:style {:color colors/gray :font-size 15}}
(i18n/label :t/ens-no-usernames)])]])
(views/defview main []
(views/letsubs [names [:account/usernames]]
[react/view {:style {:flex 1}}
[status-bar/status-bar {:type :main}]
[toolbar/simple-toolbar
(i18n/label :t/ens-usernames)]
(if (seq names)
[registered names]
[welcome])]))

View File

@ -112,10 +112,6 @@
extensions-settings (vals (get extensions :settings))]
[react/view
[profile.components/settings-title (i18n/label :t/settings)]
[profile.components/settings-item {:label-kw :t/ens-names
:action-fn #(re-frame/dispatch [:profile.ui/ens-names-button-pressed])
:accessibility-label :ens-names-button}]
[profile.components/settings-item-separator]
[profile.components/settings-item {:label-kw :t/main-currency
:value (:code currency)
:action-fn #(re-frame/dispatch [:navigate-to :currency-settings])
@ -286,6 +282,22 @@
:accessory-value active-contacts-count
:action-fn #(re-frame/dispatch [:navigate-to :contacts-list])}])
(defn- ens-item [ens {:keys [registrar] :as props}]
[list.views/big-list-item
(let [enabled? (not (nil? registrar))]
(merge
{:text (or ens (i18n/label :t/ens-usernames))
:subtext (if enabled?
(if ens (i18n/label :t/ens-your-your-name) (i18n/label :t/ens-usernames-details))
(i18n/label :t/ens-network-restriction))
:icon :main-icons/username
:accessibility-label :ens-button}
(if enabled?
{:action-fn #(re-frame/dispatch [:navigate-to :ens-main props])}
{:icon-color colors/gray
:active? false
:hide-chevron? (not enabled?)})))])
(defn tribute-to-talk-item
[opts]
[list.views/big-list-item
@ -312,7 +324,8 @@
login-data [:accounts/login]
scroll (reagent/atom nil)
active-contacts-count [:contacts/active-count]
tribute-to-talk [:tribute-to-talk/profile]]
tribute-to-talk [:tribute-to-talk/profile]
stateofus-registrar [:ens.stateofus/registrar]]
(let [shown-account (merge current-account changed-account)
;; We scroll on the component once rendered. setTimeout is necessary,
;; likely to allow the animation to finish.
@ -344,6 +357,7 @@
profile-icon-options)
:on-change-text-event :my-profile/update-name}]]
[share-profile-item (dissoc current-account :mnemonic)]
[ens-item nil {:registrar stateofus-registrar}]
[contacts-list-item active-contacts-count]
(when tribute-to-talk
[tribute-to-talk-item tribute-to-talk])

View File

@ -5,6 +5,10 @@
{:name :profile-stack
:screens (cond-> [:my-profile
:contacts-list
:ens-main
:ens-register
:ens-terms
:ens-name-details
:blocked-users-list
:profile-photo-capture
:about-app

View File

@ -15,6 +15,7 @@
[status-im.ui.screens.browser.open-dapp.views :as open-dapp]
[status-im.ui.screens.browser.views :as browser]
[status-im.ui.screens.chat.views :as chat]
[status-im.ui.screens.ens.views :as ens]
[status-im.ui.screens.contacts-list.views :as contacts-list]
[status-im.ui.screens.currency-settings.views :as currency-settings]
[status-im.ui.screens.dapps-permissions.views :as dapps-permissions]
@ -126,6 +127,10 @@
:my-profile profile.user/my-profile
:my-profile-ext-settings profile.user/extensions-settings
:contacts-list contacts-list/contacts-list
:ens-main ens/main
:ens-register ens/register
:ens-terms ens/terms
:ens-name-details ens/name-details
:blocked-users-list contacts-list/blocked-users-list
:profile-photo-capture photo-capture/profile-photo-capture
:about-app about-app/about-app

View File

@ -0,0 +1,9 @@
(ns status-im.test.ethereum.stateofus
(:require [cljs.test :refer-macros [deftest is testing]]
[status-im.ethereum.stateofus :as stateofus]))
(deftest valid-username?
(is (false? (stateofus/valid-username? nil)))
(is (true? (stateofus/valid-username? "andrey")))
(is (false? (stateofus/valid-username? "Andrey")))
(is (true? (stateofus/valid-username? "andrey12"))))

View File

@ -26,6 +26,7 @@
[status-im.test.ethereum.ens]
[status-im.test.ethereum.mnemonic]
[status-im.test.extensions.core]
[status-im.test.ethereum.stateofus]
[status-im.test.extensions.ethereum]
[status-im.test.fleet.core]
[status-im.test.group-chats.core]
@ -105,6 +106,7 @@
'status-im.test.ethereum.eip681
'status-im.test.ethereum.ens
'status-im.test.ethereum.mnemonic
'status-im.test.ethereum.stateofus
'status-im.test.extensions.core
'status-im.test.extensions.ethereum
'status-im.test.fleet.core

View File

@ -294,6 +294,7 @@
"network-id": "Network ID",
"connection-problem": "Messages connection problem",
"contact-code": "Contact code",
"key": "Key",
"enter-ens-or-contact-code": "Enter ENS username or contact code",
"transactions-delete-content": "Transaction will be removed from 'Unsigned' list",
"home": "Home",
@ -1073,5 +1074,68 @@
"signing-phrase" : "Signing phrase",
"network-fee" : "Network fee",
"sign-with-password" : "Sign with password",
"signing-a-message" : "Signing a message"
"signing-a-message" : "Signing a message",
"etherscan-lookup": "Look up on Etherscan",
"retry": "Retry",
"ens-deposit": "Deposit",
"ens-register": "Register",
"ens-wallet-address": "Wallet address",
"ens-terms-registration": "Terms of name registration.",
"ens-terms-header": "Terms of name registration",
"ens-terms-point-1": "Funds are deposited for 1 year. Your SNT will be locked, but not spent.",
"ens-terms-point-2": "After 1 year, you can release the name and get your deposit back, or take no action to keep the name.",
"ens-terms-point-3": "If terms of the contract change — e.g. Status makes contract upgrades — user has the right to release the username regardless of time held.",
"ens-terms-point-4": "The contract controller cannot access your deposited funds. They can only be moved back to the address that sent them.",
"ens-terms-point-5": "Your address(es) will be publicly associated with your ENS name.",
"ens-terms-point-6": "Usernames are created as subdomain nodes of stateofus.eth and are subject to the ENS smart contract terms.",
"ens-terms-point-7": "You authorize the contract to transfer SNT on your behalf. This can only occur when you approve a transaction to authorize the transfer.",
"ens-terms-point-8": "These terms are guaranteed by the smart contract logic at addresses:",
"ens-terms-point-9": "{{address}} (Status UsernameRegistrar) ",
"ens-terms-point-10": "0x314159265dd8dbb310642f98f50c066173c1259b (ENS Registry).",
"ens-usernames": "ENS usernames",
"ens-your-username": "Your username",
"ens-your-your-name": "Your ENS name",
"ens-usernames-details": "Register a universal username to be easily recognized by other users",
"ens-network-restriction": "Only available on Mainnet or ropsten",
"ens-custom-domain": "Custom domain",
"ens-want-domain": "I want a stateofus.eth domain",
"ens-want-custom-domain": "I own a name on another domain",
"ens-got-it": "Ok, got it",
"ens-agree-to": "Agree to ",
"ens-10-SNT": "10 SNT",
"ens-username-hints": "At least 4 characters. Latin letters, numbers, and lowercase only.",
"ens-username-invalid": "Letters and numbers only",
"ens-username-registrable": "✓ Username available!",
"ens-username-unregistrable": "Username already taken:(",
"ens-username-owned": "✓ Username is owned by you. ",
"ens-username-owned-continue": "Continuing will connect this username with your key.",
"ens-username-connected": "This user name is owned by you and connected with your Chat key.",
"ens-custom-username-unregistrable": "Username doesnt belong to you :(",
"ens-custom-username-hints": "Type the entire username including the custom domain like username.domain.eth",
"ens-welcome-hints": "ENS names transform those crazy-long addresses into unique usernames.",
"ens-welcome-point-1-title": "Simplify your ETH address",
"ens-welcome-point-1": "Your complex wallet address (0x...) becomes an easy to read, remember & share URL: ",
"ens-welcome-point-2-title": "10 SNT to register",
"ens-welcome-point-2": "Register once to keep the name forever. After 1 year, you can release the name and get your SNT back.",
"ens-welcome-point-3-title": "Connect & get paid",
"ens-welcome-point-3": "Share your name to chat on Status or receive ETH and tokens",
"ens-welcome-point-4-title": "Already own a username?",
"ens-welcome-point-4": "You can verify and add any usernames you own in the next steps.",
"ens-get-name": "Get a universal username",
"ens-registered-title": "Nice!\nThe name is yours once the transaction is complete.",
"ens-saved-title": "Username added",
"ens-registration-failed-title": "Transaction failed",
"ens-registered": "You can follow the progress in the Transaction History section of your wallet.",
"ens-saved": " is now connected with your key and can be used in Status.",
"ens-registration-failed": "To register the username, please try again.",
"ens-powered-by": "Powered by Ethereum Name Services",
"ens-add-username": "Add username",
"ens-remove-username": "Remove username",
"ens-remove-hints": "Removing will detach the username from your key.",
"ens-locked": "Username locked. You wont be able to release it until 25.05.2020",
"ens-release-username": "Release username",
"ens-your-usernames": "Your usernames",
"ens-no-usernames": "You don't have any username connected",
"ens-understand": "I understand that my wallet address will be publicly connected to my username.",
"ens-transaction-pending": "Transaction pending..."
}