feat: add encoded data for profile sharing url (#18019)

This commit is contained in:
yqrashawn 2023-12-11 20:35:39 +08:00 committed by GitHub
parent 9c6584f91b
commit 72f518d70b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 245 additions and 158 deletions

View File

@ -8,7 +8,23 @@
(def ^:private ?customization-color
[:or :string :keyword])
(def ^:private ?public-key
[:re #"^0x04[0-9a-f]{128}$"])
(def ^:private ?rpc-call
[:sequential
{:min 1}
[:map
{:closed true}
[:method :string]
[:params [:sequential :any]]
[:js-response {:optional true} :any]
[:on-success [:or fn? [:cat keyword? [:* :any]]]]
[:on-error [:or fn? [:cat keyword? [:* :any]]]]]])
(defn register-schemas
[]
(registry/register ::theme ?theme)
(registry/register ::customization-color ?customization-color))
(registry/register ::customization-color ?customization-color)
(registry/register ::public-key ?public-key)
(registry/register ::rpc-call ?rpc-call))

10
src/schema/re_frame.cljs Normal file
View File

@ -0,0 +1,10 @@
(ns schema.re-frame
(:require
[schema.registry :as registry]))
(def ^:private ?cofx
[:map])
(defn register-schemas
[]
(registry/register ::cofx ?cofx))

View File

@ -3,10 +3,8 @@
[clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.multiaccounts.update.core :as multiaccounts.update]
[status-im.ui.components.list-selection :as list-selection]
[status-im.ui.components.react :as react]
[utils.re-frame :as rf]
[utils.universal-links :as universal-links]))
[utils.re-frame :as rf]))
(re-frame/reg-fx
:copy-to-clipboard
@ -37,12 +35,6 @@
100)]
(swap! tooltips assoc tooltip-id {:opacity 1.0 :interval-id interval-id :cnt 0}))))))
(re-frame/reg-fx
:profile/share-profile-link
(fn [contact-code]
(let [link (universal-links/generate-link :user :external contact-code)]
(list-selection/open-share {:message link}))))
(rf/defn finish-success
{:events [:my-profile/finish-success]}
[{:keys [db] :as cofx}]
@ -81,11 +73,6 @@
[_ tooltip-id]
{:show-tooltip tooltip-id})
(rf/defn share-profile-link
{:events [:profile/share-profile-link]}
[_ value]
{:profile/share-profile-link value})
(rf/defn show-profile
{:events [:chat.ui/show-profile]}
[{:keys [db]} identity ens-name]

View File

@ -3,21 +3,16 @@
[re-frame.core :as re-frame]
[status-im.ui.components.react :as react]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[utils.universal-links :as universal-links]))
[utils.re-frame :as rf]))
(re-frame/reg-fx
::share
(fn [content]
(.share ^js react/sharing (clj->js content))))
(rf/defn share-link
{:events [:invite.events/share-link]}
[{:keys [db]}]
(let [{:keys [public-key preferred-name]} (get db :profile/profile)
profile-link (universal-links/generate-link :user
:external
(or preferred-name
public-key))
message (i18n/label :t/join-me {:url profile-link})]
{::share {:message message}}))
(rf/reg-event-fx
:invite.events/share-link
(fn [{:keys [db]}]
(let [{:keys [universal-profile-url]} (get db :profile/profile)
message (i18n/label :t/join-me {:url universal-profile-url})]
{::share {:message message}})))

View File

@ -21,55 +21,54 @@
[status-im2.config :as config]
[status-im2.contexts.profile.utils :as profile.utils]
[utils.ens.stateofus :as stateofus]
[utils.i18n :as i18n]
[utils.universal-links :as universal-links])
[utils.i18n :as i18n])
(:require-macros [status-im.utils.views :as views]))
(views/defview share-chat-key
[]
(views/letsubs [{:keys [address ens-name]} [:popover/popover]
width (reagent/atom nil)]
(let [link (universal-links/generate-link :user :external (or ens-name address))]
[react/view {:on-layout #(reset! width (-> ^js % .-nativeEvent .-layout .-width))}
[react/view {:style {:padding-top 16 :padding-horizontal 16}}
(when @width
[qr-codes/qr-code
{:url address
:size (- @width 32)}])
(when ens-name
[react/view
[copyable-text/copyable-text-view
{:label :t/ens-username
:container-style {:margin-top 12 :margin-bottom 4}
:copied-text ens-name}
[quo/text
{:monospace true
:accessibility-label :ens-username}
ens-name]]
[react/view
{:height 1
:margin-top 12
:margin-horizontal -16
:background-color colors/gray-lighter}]])
[copyable-text/copyable-text-view
{:label :t/chat-key
:container-style {:margin-top 12 :margin-bottom 4}
:copied-text address}
[quo/text
{:number-of-lines 1
:ellipsize-mode :middle
:accessibility-label :chat-key
:monospace true}
address]]]
[react/view styles/share-link-button
[quo/button
{:on-press (fn []
(re-frame/dispatch [:hide-popover])
(js/setTimeout
#(list-selection/open-share {:message link})
250))
:accessibility-label :share-my-contact-code-button}
(i18n/label :t/share-link)]]])))
(views/letsubs [{:keys [address ens-name]} [:popover/popover]
{:keys [universal-profile-url]} [:profile/profile]
width (reagent/atom nil)]
[react/view {:on-layout #(reset! width (-> ^js % .-nativeEvent .-layout .-width))}
[react/view {:style {:padding-top 16 :padding-horizontal 16}}
(when @width
[qr-codes/qr-code
{:url address
:size (- @width 32)}])
(when ens-name
[react/view
[copyable-text/copyable-text-view
{:label :t/ens-username
:container-style {:margin-top 12 :margin-bottom 4}
:copied-text ens-name}
[quo/text
{:monospace true
:accessibility-label :ens-username}
ens-name]]
[react/view
{:height 1
:margin-top 12
:margin-horizontal -16
:background-color colors/gray-lighter}]])
[copyable-text/copyable-text-view
{:label :t/chat-key
:container-style {:margin-top 12 :margin-bottom 4}
:copied-text address}
[quo/text
{:number-of-lines 1
:ellipsize-mode :middle
:accessibility-label :chat-key
:monospace true}
address]]]
[react/view styles/share-link-button
[quo/button
{:on-press (fn []
(re-frame/dispatch [:hide-popover])
(js/setTimeout
#(list-selection/open-share {:message universal-profile-url})
250))
:accessibility-label :share-my-contact-code-button}
(i18n/label :t/share-link)]]]))
(defn content
[]

View File

@ -5,6 +5,7 @@
[re-frame.core :as re-frame]
[react-native.async-storage :as async-storage]
[react-native.core :as rn]
[schema.core :as schema]
[status-im2.navigation.events :as navigation]
[taoensso.timbre :as log]
[utils.ethereum.chain :as chain]
@ -16,14 +17,6 @@
{:external "https://status.app"
:internal "status-app:/"})
(def links
{:private-chat "%s/p/%s"
:community-requests "%s/cr/%s"
:community "%s/c#%s"
:group-chat "%s/g/%s"
:user "%s/u#%s"
:browse "%s/b/%s"})
(rf/defn handle-browse
[_ {:keys [url]}]
(log/info "universal-links: handling browse" url)
@ -183,6 +176,61 @@
{:db (dissoc db :universal-links/url)}
(handle-url url))))
(defn generate-profile-url
([cofx] (generate-profile-url cofx nil))
([{:keys [db]} [{:keys [public-key cb]}]]
(let [profile-public-key (get-in db [:profile/profile :public-key])
profile? (or (not public-key) (= public-key profile-public-key))
ens-name? (if profile?
(get-in db [:profile/profile :ens-name?])
(get-in db [:contacts/contacts public-key :ens-name]))
public-key (if profile? profile-public-key public-key)]
(when public-key
{:json-rpc/call
[{:method (if ens-name? "wakuext_shareUserURLWithENS" "wakuext_shareUserURLWithData")
:params [public-key]
:on-success (fn [url]
(rf/dispatch [:universal-links/save-profile-url public-key url])
(when (fn? cb) (cb)))
:on-error #(log/error "failed to wakuext_shareUserURLWithData"
{:error %
:public-key public-key})}]}))))
(schema/=> generate-profile-url
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:schema
[:?
[:map
[:public-key {:optional true} :schema.common/public-key]
[:cb {:optional true} fn?]]]]]]
[:map
[:json-rpc/call :schema.common/rpc-call]]])
(rf/reg-event-fx :universal-links/generate-profile-url generate-profile-url)
(defn save-profile-url
[{:keys [db]} [public-key url]]
(when url
{:db
(cond-> db
(get-in db [:contacts/contacts public-key])
(assoc-in [:contacts/contacts public-key :universal-profile-url] url)
(= public-key (get-in db [:profile/profile :public-key]))
(assoc-in [:profile/profile :universal-profile-url] url))}))
(schema/=> save-profile-url
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:schema [:cat :schema.common/public-key :string]]]]
[:maybe :map]])
(rf/reg-event-fx :universal-links/save-profile-url save-profile-url)
(defn unwrap-js-url
[e]
(-> e

View File

@ -1,6 +1,6 @@
(ns status-im2.common.universal-links-test
(:require
[cljs.test :refer-macros [deftest is testing]]
[cljs.test :refer-macros [deftest is are testing]]
[re-frame.core :as re-frame]
[status-im2.common.universal-links :as links]))
@ -34,3 +34,67 @@
(with-redefs [re-frame/dispatch #(reset! actual %)]
(links/url-event-listener #js {})
(is (= nil @actual)))))))
(deftest generate-profile-url
(testing "user has ens name"
(testing "it calls the ens rpc method with ens name as param"
(let [pubkey "pubkey"
db {:profile/profile {:ens-name? true :public-key pubkey}}
rst (links/generate-profile-url {:db db})]
(are [result expected] (= result expected)
"wakuext_shareUserURLWithENS" (-> rst :json-rpc/call first :method)
pubkey (-> rst :json-rpc/call first :params first)))))
(testing "user has no ens name"
(testing "it calls the ens rpc method with public keyas param"
(let [pubkey "pubkey"
db {:profile/profile {:public-key pubkey}}
rst (links/generate-profile-url {:db db})]
(are [result expected] (= result expected)
"wakuext_shareUserURLWithData" (-> rst :json-rpc/call first :method)
pubkey (-> rst :json-rpc/call first :params first)))))
(testing "contact has ens name"
(testing "it calls the ens rpc method with ens name as param"
(let [pubkey "pubkey"
ens "ensname.eth"
db {:contacts/contacts {pubkey {:ens-name ens}}}
rst (links/generate-profile-url {:db db} [{:public-key pubkey}])]
(are [result expected] (= result expected)
"wakuext_shareUserURLWithENS" (-> rst :json-rpc/call first :method)
pubkey (-> rst :json-rpc/call first :params first)))))
(testing "contact has no ens name"
(testing "it calls the ens rpc method with public keyas param"
(let [pubkey "pubkey"
db {:contacts/contacts {pubkey {:public-key pubkey}}}
rst (links/generate-profile-url {:db db} [{:public-key pubkey}])]
(are [result expected] (= result expected)
"wakuext_shareUserURLWithData" (-> rst :json-rpc/call first :method)
pubkey (-> rst :json-rpc/call first :params first))))))
(deftest save-profile-url
(testing "given a contact public key and profile url"
(testing "it updates the contact in db"
(let [pubkey "pubkey"
url "url"
db {:contacts/contacts {pubkey {:public-key pubkey}}}
rst (links/save-profile-url {:db db} [pubkey url])]
(is (= (get-in rst [:db :contacts/contacts pubkey :universal-profile-url]) url)))))
(testing "given a user public key and profile url"
(testing "it updates the user profile in db"
(let [pubkey "pubkey"
url "url"
db {:profile/profile {:public-key pubkey}}
rst (links/save-profile-url {:db db} [pubkey url])]
(is (= (get-in rst [:db :profile/profile :universal-profile-url]) url)))))
(testing "given a invalid url"
(testing "it returns the db untouched"
(let [pubkey "pubkey"
url "url"
db {:profile/profile {:public-key pubkey}}
rst (links/save-profile-url {:db db} ["invalid pubkey" url])]
(is (= (:db rst) db)))))
(testing "given a nil as url"
(testing "it returns nil"
(let [pubkey "pubkey"
db {:profile/profile {:public-key pubkey}}
rst (links/save-profile-url {:db db} ["invalid pubkey"])]
(is (nil? rst))))))

View File

@ -25,7 +25,6 @@
[status-im2.contexts.profile.settings.events :as profile.settings.events]
[status-im2.contexts.push-notifications.events :as notifications]
[status-im2.contexts.shell.activity-center.events :as activity-center]
[status-im2.contexts.wallet.events :as wallet]
[status-im2.navigation.events :as navigation]
[taoensso.timbre :as log]
[utils.re-frame :as rf]
@ -85,7 +84,9 @@
:networks/current-network current-network
:networks/networks (merge networks config/default-networks-by-id)
:profile/profile (merge profile-overview settings))
(assoc-in [:wallet :ui :tokens-loading?] true))}
(assoc-in [:wallet :ui :tokens-loading?] true))
:fx [[:dispatch [:wallet/get-ethereum-chains]]
[:dispatch [:universal-links/generate-profile-url]]]}
(notifications/load-preferences)
(data-store.chats/fetch-chats-preview
{:on-success
@ -100,7 +101,6 @@
(activity-center/notifications-fetch-pending-contact-requests)
(activity-center/update-seen-state)
(activity-center/notifications-fetch-unread-count)
(wallet/get-ethereum-chains)
(redirect-to-root))))
;; login phase 2, we want to load and show chats faster so we split login into 2 phases

View File

@ -68,7 +68,7 @@
(str (name network) ":")))
(def ^:private profile-link
"https://status.app/u#zQ34e1zlOdas0pKnvrweeedsasas12adjie8")
"https://status.app/u/CwWACgkKB0VlZWVlZWUD#zQ3shUeRSwU6rnUk5JfK2k5HRiM5Hy3wU3UZQrKVzopmAHcQv")
(def ^:private wallet-address "0x39cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2")

View File

@ -14,7 +14,6 @@
[status-im2.contexts.shell.share.style :as style]
[utils.address :as address]
[utils.i18n :as i18n]
[utils.image-server :as image-server]
[utils.re-frame :as rf]))
(defn header
@ -47,27 +46,25 @@
(defn profile-tab
[]
(let [{:keys [emoji-hash
compressed-key
customization-color]
customization-color
universal-profile-url]
:as profile} (rf/sub [:profile/profile])
profile-url (str image-server/status-profile-base-url compressed-key)
abbreviated-url (address/get-abbreviated-profile-url
image-server/status-profile-base-url-without-https
compressed-key)
universal-profile-url)
emoji-hash-string (string/join emoji-hash)]
[:<>
[rn/view {:style style/qr-code-container}
[qr-codes/share-qr-code
{:type :profile
:unblur-on-android? true
:qr-data profile-url
:qr-data universal-profile-url
:qr-data-label-shown abbreviated-url
:on-share-press #(list-selection/open-share {:message profile-url})
:on-share-press #(list-selection/open-share {:message universal-profile-url})
:on-text-press #(rf/dispatch [:share/copy-text-and-show-toast
{:text-to-copy profile-url
{:text-to-copy universal-profile-url
:post-copy-message (i18n/label :t/link-to-profile-copied)}])
:on-text-long-press #(rf/dispatch [:share/copy-text-and-show-toast
{:text-to-copy profile-url
{:text-to-copy universal-profile-url
:post-copy-message (i18n/label :t/link-to-profile-copied)}])
:profile-picture (:uri (profile.utils/photo profile))
:full-name (profile.utils/displayed-name profile)

View File

@ -190,14 +190,14 @@
(first derived-address-details)]))]
{:fx [[:dispatch [:wallet/create-derived-addresses account-details on-success]]]})))
(rf/defn get-ethereum-chains
{:events [:wallet/get-ethereum-chains]}
[{:keys [db]}]
{:fx [[:json-rpc/call
[{:method "wallet_getEthereumChains"
:params []
:on-success [:wallet/get-ethereum-chains-success]
:on-error #(log/info "failed to get networks " %)}]]]})
(rf/reg-event-fx
:wallet/get-ethereum-chains
(fn [_]
{:json-rpc/call
[{:method "wallet_getEthereumChains"
:params []
:on-success [:wallet/get-ethereum-chains-success]
:on-error #(log/info "failed to get networks " %)}]}))
(rf/reg-event-fx
:wallet/get-ethereum-chains-success

View File

@ -9,6 +9,7 @@
malli.util
schema.common
[schema.core :as schema]
schema.re-frame
schema.registry
[taoensso.timbre :as log]))
@ -76,7 +77,8 @@
`:schema.common/theme`."
[]
(schema.registry/merge (malli.util/schemas))
(schema.common/register-schemas))
(schema.common/register-schemas)
(schema.re-frame/register-schemas))
(defn setup!
"Configure Malli and initializes instrumentation.

View File

@ -42,22 +42,22 @@
(get-shortened-key (eip55/address->checksum (normalized-hex address)))))
(defn get-abbreviated-profile-url
"The goal here is to generate a string that begins with
status.app/u# joined with the 1st 5 characters
of the compressed public key followed by an ellipsis followed by
the last 10 characters of the compressed public key"
[base-url public-key]
(if (and public-key base-url (> (count public-key) 17) (= "status.app/u#" base-url))
(let [first-part-of-public-pk (subs public-key 0 5)
ellipsis "..."
public-key-size (count public-key)
last-part-of-public-key (subs public-key (- public-key-size 10) public-key-size)
abbreviated-url (str base-url
first-part-of-public-pk
ellipsis
last-part-of-public-key)]
abbreviated-url)
nil))
"The goal here is to generate a string that begins with status.app/u/ joined
with the 1st 5 characters of the encoded data followed by an ellipsis
followed by the last 10 characters of the compressed public key"
[universal-profile-url]
(when-let [re-find-result (re-find #"^https://(status.app/u/)(.*)#(.*)$" universal-profile-url)]
(let [[_whole-url base-url encoded-data public-key] re-find-result]
(when (> (count public-key) 9)
(let [first-part-of-encoded-data (subs encoded-data 0 5)
ellipsis "..."
public-key-size (count public-key)
last-part-of-public-key (subs public-key (- public-key-size 10) public-key-size)
abbreviated-url (str base-url
first-part-of-encoded-data
ellipsis
last-part-of-public-key)]
abbreviated-url)))))
(defn get-shortened-compressed-key
"The goal here is to generate a string that begins with 1st 3

View File

@ -22,22 +22,17 @@
(deftest test-get-abbreviated-profile-url
(testing "Ensure the function correctly generates an abbreviated profile URL for a valid public key"
(is (= "status.app/u#zQ3sh...mrdYpzeFUa"
(is (= "status.app/u/abcde...mrdYpzeFUa"
(utils.address/get-abbreviated-profile-url
"status.app/u#"
"zQ3shPrnUhhR42JJn3QdhodGest8w8MjiH8hPaimrdYpzeFUa"))))
"https://status.app/u/abcdefg#zQ3shPrnUhhR42JJn3QdhodGest8w8MjiH8hPaimrdYpzeFUa"))))
(testing "Ensure the function returns nil when given an empty public key"
(is (nil? (utils.address/get-abbreviated-profile-url "status.app/u#" ""))))
(testing "Ensure the function returns nil when given a nil public key"
(is (nil? (utils.address/get-abbreviated-profile-url "status.app/u#" nil))))
(is (nil? (utils.address/get-abbreviated-profile-url "status.app/u/abcdefg")))
(is (nil? (utils.address/get-abbreviated-profile-url "status.app/u/abcdefg#"))))
(testing "Ensure the function returns nil when given an incorrect base URL"
(is (nil? (utils.address/get-abbreviated-profile-url
"status.app/uwu#"
"zQ3shPrnUhhR42JJn3QdhodGest8w8MjiH8hPaimrdYpzeFUa"))))
"https://status.app/uwu/abcdefg#zQ3shPrnUhhR42JJn3QdhodGest8w8MjiH8hPaimrdYpzeFUa"))))
(testing "Ensure the function returns nil when given a public key shorter than 17 characters"
(is (nil? (utils.address/get-abbreviated-profile-url "status.app/u#" "abc")))
(is (nil? (utils.address/get-abbreviated-profile-url "status.app/u#" "1234")))))
(testing "Ensure the function returns nil when given a public key shorter than 10 characters"
(is (nil? (utils.address/get-abbreviated-profile-url "https://status.app/u/abcdefg#012345678")))))

View File

@ -11,8 +11,6 @@
(def ^:const account-initials-action "/accountInitials")
(def ^:const contact-images-action "/contactImages")
(def ^:const generate-qr-action "/GenerateQRCode")
(def ^:const status-profile-base-url "https://status.app/u#")
(def ^:const status-profile-base-url-without-https "status.app/u#")
(defn get-font-file-ready
"setup font file and get the absolute path to it
@ -264,30 +262,6 @@
:ring? (if (nil? override-ring?) ring? override-ring?)
:ring-width ring-width})))
(defn get-account-qr-image-uri
[{:keys [key-uid public-key port qr-size]}]
(let [profile-qr-url (str status-profile-base-url public-key)
base-64-qr-url (js/btoa profile-qr-url)
profile-image-type "large"
error-correction-level (correction-level->index :highest)
superimpose-profile? true
media-server-url (str image-server-uri-prefix
port
generate-qr-action
"?level="
error-correction-level
"&url="
base-64-qr-url
"&keyUid="
key-uid
"&allowProfileImage="
superimpose-profile?
"&size="
(* 2 qr-size)
"&imageName="
profile-image-type)]
media-server-url))
(defn get-qr-image-uri-for-any-url
[{:keys [url port qr-size error-level]}]
(let [qr-url-base64 (js/btoa url)