From 1b6d51ff11064f4ff2799015f750dc3f0ba4e058 Mon Sep 17 00:00:00 2001 From: Andrey Shovkoplyas Date: Tue, 2 Oct 2018 16:14:35 +0200 Subject: [PATCH] implemented functionality to request user action from dapps fixed qr code js api Signed-off-by: Goran Jovic --- resources/js/web3_init.js | 66 +++++++------ resources/js/web3_opt_in.js | 87 ++++++++++------- src/status_im/browser/core.cljs | 20 ++-- src/status_im/browser/db.cljs | 2 + src/status_im/browser/permissions.cljs | 97 ++++++++++++++----- src/status_im/constants.cljs | 1 + src/status_im/events.cljs | 14 ++- src/status_im/qr_scanner/core.cljs | 9 ++ .../ui/screens/qr_scanner/views.cljs | 19 ++-- .../status_im/test/browser/permissions.cljs | 2 +- 10 files changed, 205 insertions(+), 112 deletions(-) diff --git a/resources/js/web3_init.js b/resources/js/web3_init.js index 89e8d2fc38..f192ec4adc 100644 --- a/resources/js/web3_init.js +++ b/resources/js/web3_init.js @@ -6,8 +6,9 @@ function bridgeSend(data){ WebViewBridge.send(JSON.stringify(data)); } -function sendAPIrequest(permission) { +function sendAPIrequest(permission, params) { var messageId = callbackId++; + var params = params || {}; bridgeSend({ type: 'api-request', @@ -16,7 +17,30 @@ function sendAPIrequest(permission) { host: window.location.hostname }); - return new Promise(function (resolve, reject) { callbacks[messageId] = {resolve: resolve, reject: reject};}); + return new Promise(function (resolve, reject) { + params['resolve'] = resolve; + params['reject'] = reject; + callbacks[messageId] = params; + }); +} + +function qrCodeResponse(data, callback){ + var result = data.data; + var regex = new RegExp(callback.regex); + if (!result) { + if (callback.reject) { + callback.reject(new Error("Cancelled")); + } + } + else if (regex.test(result)) { + if (callback.resolve) { + callback.resolve(result); + } + } else { + if (callback.reject) { + callback.reject(new Error("Doesn't match")); + } + } } WebViewBridge.onMessage = function (message) { @@ -25,9 +49,10 @@ WebViewBridge.onMessage = function (message) { var callback = callbacks[id]; if (callback) { - if (data.type === "api-response") { - if (data.isAllowed) { + if (data.permission == 'qr-code'){ + qrCodeResponse(data, callback); + } else if (data.isAllowed) { callback.resolve(data.data); } else { callback.reject(new Error("Denied")); @@ -44,22 +69,6 @@ WebViewBridge.onMessage = function (message) { callback.callback(data.error, data.result); } } - } else if (data.type === "scan-qr-code-callback") { - var id = data.data.messageId; - var callback = callbacks[id]; - if (callback) { - var result = data.result; - var regex = new RegExp(callback.regex); - if (regex.test(result)) { - if (callback.resolve) { - callback.resolve(result); - } - } else { - if (callback.reject) { - callback.reject(result); - } - } - } } } }; @@ -73,9 +82,8 @@ StatusAPI.prototype.getContactCode = function () { var StatusHttpProvider = function () {}; StatusHttpProvider.prototype.isStatus = true; -StatusHttpProvider.prototype.isConnected = function () { return true; }; - StatusHttpProvider.prototype.status = new StatusAPI(); +StatusHttpProvider.prototype.isConnected = function () { return true; }; function web3Response (payload, result){ return {id: payload.id, @@ -140,19 +148,13 @@ StatusHttpProvider.prototype.sendAsync = function (payload, callback) { } }; -StatusHttpProvider.prototype.scanQRCode = function (regex) { - return new Promise(function (resolve, reject) { - var messageId = callbackId++; - callbacks[messageId] = {resolve: resolve, reject: reject, regex: regex}; - bridgeSend({type: 'scan-qr-code', - messageId: messageId}); - }); -}; - - StatusHttpProvider.prototype.enable = function () { return new Promise(function (resolve, reject) { setTimeout(resolve, 1000);}); }; + +StatusHttpProvider.prototype.scanQRCode = function (regex) { + return sendAPIrequest('qr-code', {regex: regex}); +}; } var protocol = window.location.protocol diff --git a/resources/js/web3_opt_in.js b/resources/js/web3_opt_in.js index 37c721be4f..4f23fc78b3 100644 --- a/resources/js/web3_opt_in.js +++ b/resources/js/web3_opt_in.js @@ -6,8 +6,9 @@ function bridgeSend(data){ WebViewBridge.send(JSON.stringify(data)); } -function sendAPIrequest(permission) { +function sendAPIrequest(permission, params) { var messageId = callbackId++; + var params = params || {}; bridgeSend({ type: 'api-request', @@ -16,7 +17,30 @@ function sendAPIrequest(permission) { host: window.location.hostname }); - return new Promise(function (resolve, reject) { callbacks[messageId] = {resolve: resolve, reject: reject};}); + return new Promise(function (resolve, reject) { + params['resolve'] = resolve; + params['reject'] = reject; + callbacks[messageId] = params; + }); +} + +function qrCodeResponse(data, callback){ + var result = data.data; + var regex = new RegExp(callback.regex); + if (!result) { + if (callback.reject) { + callback.reject(new Error("Cancelled")); + } + } + else if (regex.test(result)) { + if (callback.resolve) { + callback.resolve(result); + } + } else { + if (callback.reject) { + callback.reject(new Error("Doesn't match")); + } + } } WebViewBridge.onMessage = function (message) { @@ -24,38 +48,31 @@ WebViewBridge.onMessage = function (message) { var id = data.messageId; var callback = callbacks[id]; - if (callback) - { - if (data.type === "api-response") - { - if (data.isAllowed) - { - if (data.permission == 'web3') - { + if (callback) { + if (data.type === "api-response") { + if (data.permission == 'qr-code'){ + qrCodeResponse(data, callback); + } else if (data.isAllowed) { + if (data.permission == 'web3') { window.currentAccountAddress = data.data; callback.resolve(); - } - else - { + } else { callback.resolve(data.data); } - } - else - { + } else { callback.reject(new Error("Denied")); } - } - else if (data.type === "web3-send-async-callback") - { - if (callback.results) - { - callback.results.push(data.error || data.result); - if (callback.results.length == callback.num) - callback.callback(undefined, callback.results); - } - else - { - callback.callback(data.error, data.result); + } else if (data.type === "web3-send-async-callback") { + var id = data.messageId; + var callback = callbacks[id]; + if (callback) { + if (callback.results) { + callback.results.push(data.error || data.result); + if (callback.results.length == callback.num) + callback.callback(undefined, callback.results); + } else { + callback.callback(data.error, data.result); + } } } } @@ -91,9 +108,16 @@ StatusAPI.prototype.getContactCode = function () { var ReadOnlyProvider = function () {}; ReadOnlyProvider.prototype.isStatus = true; +ReadOnlyProvider.prototype.status = new StatusAPI(); ReadOnlyProvider.prototype.isConnected = function () { return true; }; -ReadOnlyProvider.prototype.status = new StatusAPI(); +ReadOnlyProvider.prototype.enable = function () { + return sendAPIrequest('web3'); +}; + +ReadOnlyProvider.prototype.scanQRCode = function (regex) { + return sendAPIrequest('qr-code', {regex: regex}); +}; ReadOnlyProvider.prototype.send = function (payload) { if (payload.method == "eth_uninstallFilter"){ @@ -107,10 +131,6 @@ ReadOnlyProvider.prototype.send = function (payload) { } }; -ReadOnlyProvider.prototype.enable = function () { - return sendAPIrequest('web3'); -}; - ReadOnlyProvider.prototype.sendAsync = function (payload, callback) { var syncResponse = getSyncResponse(payload); @@ -143,7 +163,6 @@ ReadOnlyProvider.prototype.sendAsync = function (payload, callback) { } }; - } console.log("ReadOnlyProvider"); diff --git a/src/status_im/browser/core.cljs b/src/status_im/browser/core.cljs index 2b479ece16..c58a3751a9 100644 --- a/src/status_im/browser/core.cljs +++ b/src/status_im/browser/core.cljs @@ -257,11 +257,18 @@ (web3-send-async cofx payload message-id)))) (fx/defn handle-scanned-qr-code - [cofx data message] - (fx/merge cofx - (send-to-bridge (assoc message :result data)) + [cofx data {:keys [dapp-name permission message-id]}] + (fx/merge (assoc-in cofx [:db :browser/options :yielding-control?] false) + (browser.permissions/send-response-to-bridge permission message-id true data) + (browser.permissions/process-next-permission dapp-name) (navigation/navigate-back))) +(fx/defn handle-canceled-qr-code + [cofx {:keys [dapp-name permission message-id]}] + (fx/merge (assoc-in cofx [:db :browser/options :yielding-control?] false) + (browser.permissions/send-response-to-bridge permission message-id true nil) + (browser.permissions/process-next-permission dapp-name))) + (fx/defn process-bridge-message [{:keys [db] :as cofx} message] (let [{:browser/keys [options browsers]} db @@ -283,13 +290,6 @@ (= type constants/web3-send-async-read-only) (web3-send-async-read-only cofx dapp-name payload messageId) - (= type constants/scan-qr-code) - (qr-scanner/scan-qr-code cofx - {:modal? false} - (merge {:handler :browser.bridge.callback/qr-code-scanned} - {:type constants/scan-qr-code-callback - :data data})) - (= type constants/api-request) (browser.permissions/process-permission cofx dapp-name permission messageId)))) diff --git a/src/status_im/browser/db.cljs b/src/status_im/browser/db.cljs index 5cc373e839..693a12b69e 100644 --- a/src/status_im/browser/db.cljs +++ b/src/status_im/browser/db.cljs @@ -16,6 +16,7 @@ (spec/def :browser/show-tooltip (spec/nilable keyword?)) (spec/def :browser/show-permission (spec/nilable map?)) (spec/def :browser/pending-permissions (spec/nilable list?)) +(spec/def :browser/yielding-control? (spec/nilable boolean?)) (spec/def :browser/options (spec/nilable @@ -27,6 +28,7 @@ :browser/show-tooltip :browser/show-permission :browser/pending-permissions + :browser/yielding-control? :browser/error?]))) (spec/def :browser/browser diff --git a/src/status_im/browser/permissions.cljs b/src/status_im/browser/permissions.cljs index ed877288bf..79a1a8ddf3 100644 --- a/src/status_im/browser/permissions.cljs +++ b/src/status_im/browser/permissions.cljs @@ -3,16 +3,39 @@ [status-im.data-store.dapp-permissions :as dapp-permissions] [status-im.i18n :as i18n] [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.fx :as fx])) + [status-im.utils.fx :as fx] + [status-im.qr-scanner.core :as qr-scanner])) (def supported-permissions - {constants/dapp-permission-contact-code {:title (i18n/label :t/wants-to-access-profile) + {constants/dapp-permission-qr-code {:yield-control? true + :allowed? true} + constants/dapp-permission-contact-code {:title (i18n/label :t/wants-to-access-profile) :description (i18n/label :t/your-contact-code) :icon :icons/profile-active} constants/dapp-permission-web3 {:title (i18n/label :t/dapp-would-like-to-connect-wallet) :description (i18n/label :t/allowing-authorizes-this-dapp) :icon :icons/wallet-active}}) +(fx/defn permission-yield-control + [{:keys [db] :as cofx} dapp-name permission message-id] + (cond + (= permission constants/dapp-permission-qr-code) + (fx/merge (assoc-in cofx [:db :browser/options :yielding-control?] true) + (qr-scanner/scan-qr-code {:modal? false} + {:handler :browser.bridge.callback/qr-code-scanned + :cancel-handler :browser.bridge.callback/qr-code-canceled + :data {:dapp-name dapp-name + :permission permission + :message-id message-id}})))) + +(fx/defn permission-show-permission + [{:keys [db] :as cofx} dapp-name permission message-id yield-control?] + {:db (assoc-in db [:browser/options :show-permission] + {:requested-permission permission + :message-id message-id + :dapp-name dapp-name + :yield-control? yield-control?})}) + (defn get-permission-data [cofx allowed-permission] (let [account (get-in cofx [:db :account/account])] (get {constants/dapp-permission-contact-code (:public-key account) @@ -22,13 +45,13 @@ (fx/defn send-response-to-bridge "Send response to the bridge. If the permission is allowed, send data associated with the permission" - [{:keys [db] :as cofx} permission message-id allowed?] + [{:keys [db] :as cofx} permission message-id allowed? data] {:browser/send-to-bridge {:message (cond-> {:type constants/api-response :isAllowed allowed? :permission permission :messageId message-id} allowed? - (assoc :data (get-permission-data cofx permission))) + (assoc :data data)) :webview (:webview-bridge db)}}) (fx/defn update-dapp-permissions @@ -47,33 +70,46 @@ if there is no pending permissions left, save all granted permissions and return the result to the bridge" [{:keys [db] :as cofx} dapp-name] - (if (get-in db [:browser/options :show-permission]) - {:db db} - (let [pending-permissions (get-in db [:browser/options :pending-permissions]) - next-permission (last pending-permissions)] - (when next-permission - {:db (-> db - (update-in [:browser/options :pending-permissions] butlast) - (assoc-in [:browser/options :show-permission] - {:requested-permission (:permission next-permission) - :message-id (:message-id next-permission) - :dapp-name dapp-name}))})))) + (let [{:keys [show-permission yielding-control?]} (get db :browser/options)] + (if (or show-permission yielding-control?) + {:db db} + (let [pending-permissions (get-in db [:browser/options :pending-permissions]) + next-permission (last pending-permissions) + new-cofx (update-in cofx [:db :browser/options :pending-permissions] butlast)] + (when-let [{:keys [yield-control? permission message-id allowed?]} next-permission] + (if (and yield-control? allowed?) + (permission-yield-control new-cofx dapp-name permission message-id) + (permission-show-permission new-cofx dapp-name permission message-id yield-control?))))))) + +(fx/defn send-response-and-process-next-permission + [{:keys [db] :as cofx} dapp-name requested-permission message-id] + (fx/merge cofx + (send-response-to-bridge requested-permission + message-id + true + (get-permission-data cofx requested-permission)) + (process-next-permission dapp-name))) (fx/defn allow-permission "Add permission to set of allowed permission and process next permission" [{:keys [db] :as cofx}] - (let [{:keys [requested-permission message-id dapp-name]} (get-in db [:browser/options :show-permission])] + (let [{:keys [requested-permission message-id dapp-name yield-control?]} + (get-in db [:browser/options :show-permission])] (fx/merge (assoc-in cofx [:db :browser/options :show-permission] nil) (update-dapp-permissions dapp-name requested-permission true) - (send-response-to-bridge requested-permission message-id true) - (process-next-permission dapp-name)))) + (if yield-control? + (permission-yield-control dapp-name requested-permission message-id) + (send-response-and-process-next-permission dapp-name requested-permission message-id))))) (fx/defn deny-permission "Add permission to set of allowed permission and process next permission" [{:keys [db] :as cofx}] (let [{:keys [requested-permission message-id dapp-name]} (get-in db [:browser/options :show-permission])] (fx/merge (assoc-in cofx [:db :browser/options :show-permission] nil) - (send-response-to-bridge requested-permission message-id false) + (send-response-to-bridge requested-permission + message-id + false + (get-permission-data cofx requested-permission)) (process-next-permission dapp-name)))) (fx/defn process-permission @@ -81,12 +117,21 @@ If supported permission is already granted, return the result immediatly to the bridge Otherwise process the first permission which will prompt user" [cofx dapp-name permission message-id] - (let [allowed-permissions (set (get-in cofx [:db :dapps/permissions dapp-name :permissions])) - permission-allowed? (boolean (allowed-permissions permission)) - permission-supported? ((set (keys supported-permissions)) permission)] - (if (or permission-allowed? (not permission-supported?)) - (send-response-to-bridge cofx permission message-id permission-allowed?) + (let [allowed-permissions (set (get-in cofx [:db :dapps/permissions dapp-name :permissions])) + permission-allowed? (boolean (allowed-permissions permission)) + supported-permission (get supported-permissions permission)] + (cond + (not supported-permission) + (send-response-to-bridge cofx permission message-id false nil) + + (and (or permission-allowed? (:allowed? supported-permission)) (not (:yield-control? supported-permission))) + (send-response-to-bridge cofx permission message-id true (get-permission-data cofx permission)) + + :else (process-next-permission (update-in cofx [:db :browser/options :pending-permissions] - conj {:permission permission - :message-id message-id}) + conj {:permission permission + :allowed? (or permission-allowed? + (:allowed? supported-permission)) + :yield-control? (:yield-control? supported-permission) + :message-id message-id}) dapp-name)))) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 2d558a98d8..1bd154a30f 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -191,6 +191,7 @@ (def ^:const dapp-permission-contact-code "contact-code") (def ^:const dapp-permission-web3 "web3") +(def ^:const dapp-permission-qr-code "qr-code") (def ^:const api-response "api-response") (def ^:const api-request "api-request") (def ^:const history-state-changed "history-state-changed") diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index fa6a4d9da6..abeb5da1af 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -419,8 +419,13 @@ (handlers/register-handler-fx :browser.bridge.callback/qr-code-scanned - (fn [cofx [_ _ data message]] - (browser/handle-scanned-qr-code cofx data message))) + (fn [cofx [_ _ data qr-code-data]] + (browser/handle-scanned-qr-code cofx data (:data qr-code-data)))) + +(handlers/register-handler-fx + :browser.bridge.callback/qr-code-canceled + (fn [cofx [_ _ qr-code-data]] + (browser/handle-canceled-qr-code cofx (:data qr-code-data)))) ;; qr-scanner module @@ -434,6 +439,11 @@ (fn [cofx [_ context data]] (qr-scanner/set-qr-code cofx context data))) +(handlers/register-handler-fx + :qr-scanner.callback/scan-qr-code-cancel + (fn [cofx [_ context]] + (qr-scanner/set-qr-code-cancel cofx context))) + ;; privacy-policy module (handlers/register-handler-fx diff --git a/src/status_im/qr_scanner/core.cljs b/src/status_im/qr_scanner/core.cljs index f80c1d5a52..ffd7b0852b 100644 --- a/src/status_im/qr_scanner/core.cljs +++ b/src/status_im/qr_scanner/core.cljs @@ -23,3 +23,12 @@ (dissoc :current-qr-context))} (when-let [qr-codes (:qr-codes db)] {:dispatch [(:handler qr-codes) context data (dissoc qr-codes :handler)]}))) + +(fx/defn set-qr-code-cancel + [{:keys [db]} context] + (merge {:db (-> db + (update :qr-codes dissoc context) + (dissoc :current-qr-context))} + (when-let [qr-codes (:qr-codes db)] + (when-let [handler (:cancel-handler qr-codes)] + {:dispatch [handler context qr-codes]})))) \ No newline at end of file diff --git a/src/status_im/ui/screens/qr_scanner/views.cljs b/src/status_im/ui/screens/qr_scanner/views.cljs index 4d278b5b98..e75b5e3342 100644 --- a/src/status_im/ui/screens/qr_scanner/views.cljs +++ b/src/status_im/ui/screens/qr_scanner/views.cljs @@ -7,13 +7,18 @@ [status-im.ui.components.camera :as camera] [status-im.ui.components.status-bar.view :as status-bar] [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.screens.qr-scanner.styles :as styles])) + [status-im.ui.screens.qr-scanner.styles :as styles] + [status-im.ui.components.toolbar.actions :as actions])) -(defview qr-scanner-toolbar [title hide-nav?] - (letsubs [modal [:get :modal]] - [react/view - [status-bar/status-bar] - [toolbar/simple-toolbar title]])) +(defview qr-scanner-toolbar [title identifier] + [react/view + [status-bar/status-bar] + [toolbar/toolbar nil + [toolbar/nav-button (actions/back + #(do + (re-frame/dispatch [:qr-scanner.callback/scan-qr-code-cancel identifier]) + (re-frame/dispatch [:navigate-back])))] + [toolbar/content-title title]]]) (defn on-barcode-read [identifier data] (re-frame/dispatch [:qr-scanner.callback/scan-qr-code-success identifier (camera/get-qr-code-data data)])) @@ -23,7 +28,7 @@ camera-initialized? (reagent/atom false) barcode-read? (reagent/atom false)] [react/view styles/barcode-scanner-container - [qr-scanner-toolbar (or (:toolbar-title identifier) (i18n/label :t/scan-qr)) (not @camera-initialized?)] + [qr-scanner-toolbar (or (:toolbar-title identifier) (i18n/label :t/scan-qr)) identifier] [camera/camera {:onBarCodeRead #(if (:multiple? identifier) (on-barcode-read identifier %) (when-not @barcode-read? diff --git a/test/cljs/status_im/test/browser/permissions.cljs b/test/cljs/status_im/test/browser/permissions.cljs index e57f4d7494..030b3362ac 100644 --- a/test/cljs/status_im/test/browser/permissions.cljs +++ b/test/cljs/status_im/test/browser/permissions.cljs @@ -28,7 +28,7 @@ :messageId 1 :permission "contact-code"}))] (is (= (get-in result-ask [:db :browser/options :show-permission]) - {:requested-permission "contact-code" :dapp-name "test.com" :message-id 1})) + {:requested-permission "contact-code" :dapp-name "test.com" :message-id 1 :yield-control? nil})) (is (zero? (count (get-in result-ask [:db :dapps/permissions])))) (testing "then user accepts the supported permission"