diff --git a/doc/DEV.md b/doc/DEV.md index 50e53044..484bea29 100644 --- a/doc/DEV.md +++ b/doc/DEV.md @@ -8,12 +8,17 @@ If `ghcjs` installed locally you want to remove following line from `./dev_up.sh 2. Update paths variables in `.env` file, see README.md -2. Run `./script/dev_up.sh` script. After launching: +3. Run `./script/dev_up.sh` script. After launching: - wait for sync finishing of `node` and `kupo` - run `wallet.sh` in `cardano` window - run `encoins --run` in `apps` window to launch relay server - run `./script/run.sh` in `apps` window to launch frontend - - run `./script/build_dev_js.sh` in apps window to rebuild frontend + +4. Build frontend +```shell +./script/docker_dev_run.sh` # if you use docker +./script/build_dev_js.sh` +``` ## Dev down diff --git a/frontend/encoins-frontend.cabal b/frontend/encoins-frontend.cabal index c10c5916..bad5aa1d 100644 --- a/frontend/encoins-frontend.cabal +++ b/frontend/encoins-frontend.cabal @@ -20,20 +20,22 @@ library Backend.Protocol.StrongTypes Backend.Protocol.TxValidity Backend.Protocol.Types - Backend.Protocol.Utility Backend.Servant.Client Backend.Servant.Requests Backend.Status - Backend.Utility Backend.Wallet + Common.Events + Common.Protocol + Common.Reflex.Dom.Extra + Common.Reflex.Extra + Common.Url + Common.Utility Config.Config ENCOINS.App ENCOINS.App.Body - ENCOINS.App.Widgets.Basic ENCOINS.App.Widgets.Cloud ENCOINS.App.Widgets.CloudWindow ENCOINS.App.Widgets.Coin - ENCOINS.App.Widgets.ConnectWindow ENCOINS.App.Widgets.ImportWindow ENCOINS.App.Widgets.InputAddressWindow ENCOINS.App.Widgets.MainTabs @@ -49,10 +51,9 @@ library ENCOINS.App.Widgets.TransactionBalance ENCOINS.App.Widgets.WelcomeWindow ENCOINS.Common.Cache - ENCOINS.Common.Events + ENCOINS.Common.ConnectWindow ENCOINS.Common.Head ENCOINS.Common.Language - ENCOINS.Common.Utils ENCOINS.Common.Widgets.Advanced ENCOINS.Common.Widgets.Basic ENCOINS.Common.Widgets.Connect @@ -62,13 +63,13 @@ library ENCOINS.Common.Widgets.Wallet ENCOINS.DAO ENCOINS.DAO.Body - ENCOINS.DAO.PollResults - ENCOINS.DAO.Polls ENCOINS.DAO.Widgets.DelegateWindow ENCOINS.DAO.Widgets.DelegateWindow.RelayNames + ENCOINS.DAO.Widgets.DelegateWindow.RelayTable ENCOINS.DAO.Widgets.Navbar + ENCOINS.DAO.Widgets.Poll.PollResults + ENCOINS.DAO.Widgets.Poll.Polls ENCOINS.DAO.Widgets.PollWidget - ENCOINS.DAO.Widgets.RelayTable ENCOINS.DAO.Widgets.StatusWidget ENCOINS.Website ENCOINS.Website.Body diff --git a/frontend/src/Backend/EncoinsTx.hs b/frontend/src/Backend/EncoinsTx.hs index 34af032a..843a8b45 100755 --- a/frontend/src/Backend/EncoinsTx.hs +++ b/frontend/src/Backend/EncoinsTx.hs @@ -13,38 +13,45 @@ import JS.App (walletSignTx) import PlutusTx.Prelude (length) import Reflex.Dom hiding (Input) import Servant.Reflex (BaseUrl (..)) +import Text.Hex (decodeHex) import Witherable (catMaybes) import Prelude hiding (length) +import Backend.Protocol.Fees (protocolFees) import Backend.Protocol.Setup - ( encoinsCurrencySymbol + ( bulletproofSetup + , emergentChangeAddress + , encoinsCurrencySymbol , ledgerAddress , minAdaTxOutInLedger ) import Backend.Protocol.Types -import Backend.Protocol.Utility - ( getEncoinsInUtxos - , mkLedgerRedeemer - , mkWalletRedeemer - ) import Backend.Servant.Requests import Backend.Status ( LedgerTxStatus (..) , TransferTxStatus (..) , WalletTxStatus (..) ) -import Backend.Utility +import Backend.Wallet (Wallet (..), toJS) +import Common.Events +import Common.Protocol + ( calculateV + , getEncoinsInUtxos + ) +import Common.Reflex.Dom.Extra (elementResultJS) +import Common.Reflex.Extra ( eventMaybe + , fireWhenJustThenReset , switchHoldDyn - , toEither + , updateUrls + ) +import Common.Utility + ( toEither , toText ) -import Backend.Wallet (Wallet (..), toJS) -import ENCOINS.App.Widgets.Basic (elementResultJS) import ENCOINS.BaseTypes import ENCOINS.Bulletproofs -import ENCOINS.Common.Events -import ENCOINS.Common.Widgets.Advanced (fireWhenJustThenReset, updateUrls) +import PlutusTx.Builtins encoinsTxWalletMode :: (MonadWidget t m) => @@ -394,3 +401,48 @@ encoinsTxLedgerMode <$> leftmost [eStatusError, eServerError] ] return (fmap getEncoinsInUtxos dUTXOs, eStatus) + +------------------------------------------------------------------------------- +-- Helpers +------------------------------------------------------------------------------- + +mkWalletRedeemer :: + EncoinsMode + -> Address + -> Address + -> BulletproofParams + -> Secrets + -> [MintingPolarity] + -> Randomness + -> EncoinsRedeemer +mkWalletRedeemer mode ledgerAddr changeAddr bp secrets mps rs = red + where + (_, inputs, proof) = bulletproof bulletproofSetup bp secrets mps rs + v = calculateV secrets mps + inputs' = map (\(Input g p) -> (fromGroupElement g, p)) inputs + sig = + toBuiltin $ + fromJust $ + decodeHex "" + red = ((ledgerAddr, changeAddr, protocolFees mode v), (v, inputs'), proof, sig) + +mkLedgerRedeemer :: + EncoinsMode + -> Address + -> BulletproofParams + -> Secrets + -> [MintingPolarity] + -> Randomness + -> Address + -> Maybe EncoinsRedeemer +mkLedgerRedeemer mode ledgerAddr bp secrets mps rs changeAddr = + if changeAddr == emergentChangeAddress then Nothing else Just red + where + (_, inputs, proof) = bulletproof bulletproofSetup bp secrets mps rs + v = calculateV secrets mps + inputs' = map (\(Input g p) -> (fromGroupElement g, p)) inputs + sig = + toBuiltin $ + fromJust $ + decodeHex "" + red = ((ledgerAddr, changeAddr, protocolFees mode v), (v, inputs'), proof, sig) diff --git a/frontend/src/Backend/Environment.hs b/frontend/src/Backend/Environment.hs index d684d284..834236a0 100755 --- a/frontend/src/Backend/Environment.hs +++ b/frontend/src/Backend/Environment.hs @@ -13,7 +13,7 @@ import Backend.Protocol.Fees (protocolFees) import Backend.Protocol.Setup (ledgerAddress) import Backend.Protocol.TxValidity (getAda) import Backend.Protocol.Types -import ENCOINS.App.Widgets.Basic (elementResultJS) +import Common.Reflex.Dom.Extra (elementResultJS) import ENCOINS.Bulletproofs import ENCOINS.Crypto.Field (Field (..)) import JS.App (sha2_256) @@ -47,7 +47,13 @@ getRandomness :: (MonadWidget t m) => Event t () -> m (Behavior t Randomness) getRandomness e = do eRandomness <- performEvent $ liftIO randomIO <$ e hold - ( Randomness (F 3417) (map F [1 .. 20]) (map F [21 .. 40]) (F 8532) (F 16512) (F 1235) + ( Randomness + (F 3417) + (map F [1 .. 20]) + (map F [21 .. 40]) + (F 8532) + (F 16512) + (F 1235) ) eRandomness diff --git a/frontend/src/Backend/Protocol/StrongTypes.hs b/frontend/src/Backend/Protocol/StrongTypes.hs index bad9b994..e7d68169 100644 --- a/frontend/src/Backend/Protocol/StrongTypes.hs +++ b/frontend/src/Backend/Protocol/StrongTypes.hs @@ -3,7 +3,7 @@ module Backend.Protocol.StrongTypes , toPasswordHash ) where -import Backend.Utility (hashKeccak512) +import Common.Utility (hashKeccak512) import Data.Text (Text) import qualified Data.Text as T diff --git a/frontend/src/Backend/Protocol/TxValidity.hs b/frontend/src/Backend/Protocol/TxValidity.hs index 4e9e1f76..bc014483 100755 --- a/frontend/src/Backend/Protocol/TxValidity.hs +++ b/frontend/src/Backend/Protocol/TxValidity.hs @@ -8,7 +8,7 @@ import Backend.Status ( AppStatus , isAppProcess ) -import Backend.Utility (space, toText) +import Common.Utility (space, toText) import Backend.Wallet (Wallet (..), WalletName (..), currentNetworkApp) import CSL (TransactionUnspentOutput (..), amount, coin) import Config.Config (NetworkConfig (..), networkConfig) diff --git a/frontend/src/Backend/Servant/Requests.hs b/frontend/src/Backend/Servant/Requests.hs index 8a782333..020202f0 100755 --- a/frontend/src/Backend/Servant/Requests.hs +++ b/frontend/src/Backend/Servant/Requests.hs @@ -4,9 +4,9 @@ module Backend.Servant.Requests where import Backend.Protocol.Types import Backend.Servant.Client -import Backend.Utility (normalizeCurrentUrl, normalizePingUrl) +import Common.Url (normalizeCurrentUrl, normalizePingUrl) import Config.Config (saveServerUrl) -import ENCOINS.Common.Events +import Common.Events import JS.App (pingServer) import CSL (TransactionInputs) diff --git a/frontend/src/Backend/Status.hs b/frontend/src/Backend/Status.hs index e860634d..6baff939 100755 --- a/frontend/src/Backend/Status.hs +++ b/frontend/src/Backend/Status.hs @@ -2,7 +2,7 @@ module Backend.Status where -import Backend.Utility (column, space, toText) +import Common.Utility (column, space, toText) import Data.Text (Text) import qualified Data.Text as T diff --git a/frontend/src/Backend/Utility.hs b/frontend/src/Backend/Utility.hs deleted file mode 100644 index fc18dd78..00000000 --- a/frontend/src/Backend/Utility.hs +++ /dev/null @@ -1,157 +0,0 @@ -module Backend.Utility where - -import Config.Config (NetworkId (..), appNetwork) - -import qualified CSL -import Control.Monad (join, (<=<)) -import Control.Monad.IO.Class (liftIO) -import qualified Crypto.Hash.Keccak as Keccak -import Data.Bool (bool) -import Data.ByteString.Base16 as BS16 -import Data.Foldable (foldl') -import Data.Functor ((<&>)) -import qualified Data.Map as Map -import Data.Maybe (isNothing) -import qualified Data.Set as Set -import Data.Text (Text) -import qualified Data.Text as T -import qualified Data.Text.Encoding as TE -import Data.Time (UTCTime, defaultTimeLocale, formatTime) -import qualified Data.UUID as Uid -import qualified Data.UUID.V4 as Uid -import Reflex.Dom -import Witherable (catMaybes) - --- O(n * log(n)) instead of O(n^2) in 'nubBy' --- It reverse list of tokens -nubWith :: (Ord b) => (a -> b) -> [a] -> [a] -nubWith f l = snd $ foldl' (func f) (Set.empty, []) l - where - func f' (set, acc) x - | Set.member x' set = (set, acc) - | otherwise = (Set.insert x' set, x : acc) - where - x' = f' x - --- Combine two lists efficiently excluding duplicates from both lists --- Original 'union' excludes duplicates from last list only. --- O((n+m) * log(n+m)) instead of > O(n+m^2) --- Give the priority to first list. --- It reverse list of tokens -unionWith :: (Ord b) => (a -> b) -> [a] -> [a] -> [a] -unionWith f l1 l2 = - let func f' (set, acc) x - | Set.member x' set = (set, acc) - | otherwise = (Set.insert x' set, x : acc) - where - x' = f' x - listOfUniq = snd $ foldl' (func f) (Set.empty, []) $ l1 <> l2 - in listOfUniq - -normalizePingUrl :: Text -> Text -normalizePingUrl url = T.append (T.dropWhileEnd (== '/') url) $ case appNetwork of - Mainnet -> - if T.isInfixOf "execute-api.eu-central-1.amazonaws.com" url - then "//" - else "/" - Testnet -> "/" - -normalizeCurrentUrl :: Text -> Text -normalizeCurrentUrl url = case url of - "localhost:3000" -> "http://localhost:3000" - u -> u - -toEither :: e -> Maybe a -> Either e a -toEither err Nothing = Left err -toEither _ (Just a) = Right a - -textMaybe :: Text -> Maybe Text -textMaybe txt = bool (Just txt) Nothing $ T.null txt - -switchHoldDyn :: - (MonadWidget t m) => - Dynamic t a - -> (a -> m (Event t b)) - -> m (Event t b) -switchHoldDyn da f = switchHold never <=< dyn $ da <&> f - -dynHoldDyn :: - (MonadWidget t m) => - Dynamic t a - -> b - -> (a -> m (Dynamic t b)) - -> m (Dynamic t b) -dynHoldDyn dA b f = do - edB <- dyn $ f <$> dA - join <$> holdDyn (constDyn b) edB - -isMultiAssetOf :: Text -> Text -> CSL.MultiAsset -> Bool -isMultiAssetOf symbol token (CSL.MultiAsset mp) = - case Map.lookup symbol mp of - Nothing -> False - Just i -> maybe False (const True) $ Map.lookup token i - -eventMaybe :: (Reflex t) => Event t (Maybe a) -> (Event t (), Event t a) -eventMaybe ev = (() <$ ffilter isNothing ev, catMaybes ev) - -eventMaybeDynDef :: - (Reflex t) => - Dynamic t def - -> Event t (Maybe a) - -> (Event t def, Event t a) -eventMaybeDynDef dDefault ev = (tagPromptlyDyn dDefault $ ffilter isNothing ev, catMaybes ev) - -eventTuple :: (Reflex t) => Event t (a, b) -> (Event t a, Event t b) -eventTuple ev = (fst <$> ev, snd <$> ev) - -dynTuple :: - (MonadWidget t m) => - a - -> b - -> Event t (a, b) - -> m (Dynamic t a, Dynamic t b) -dynTuple aDef bDef eAB = do - da <- holdDyn aDef $ fst <$> eAB - db <- holdDyn bDef $ snd <$> eAB - pure (da, db) - -space :: Text -space = " " - -column :: Text -column = ":" - -toText :: (Show a) => a -> Text -toText = T.pack . show - --- resId should be unique in every call of elementResultJS --- genUid generates unique resId. -genUid :: (MonadWidget t m) => Event t () -> m (Event t Text) -genUid ev = performEvent $ (Uid.toText <$> liftIO Uid.nextRandom) <$ ev - -data HashBit = B512 | B384 | B256 | B224 - deriving (Eq) - -hashKeccak :: HashBit -> Text -> Text -hashKeccak hb raw = - let keccak = case hb of - B512 -> Keccak.keccak512 - B384 -> Keccak.keccak384 - B256 -> Keccak.keccak256 - B224 -> Keccak.keccak224 - in TE.decodeUtf8 $ BS16.encode $ keccak $ TE.encodeUtf8 raw - -hashKeccak512 :: Text -> Text -hashKeccak512 = hashKeccak B512 - -hashKeccak256 :: Text -> Text -hashKeccak256 = hashKeccak B256 - -isHashOfRaw :: Text -> Text -> Bool -isHashOfRaw hash raw = hash == hashKeccak512 raw - -formatPollTime :: UTCTime -> Text -formatPollTime = T.pack . formatTime defaultTimeLocale "%e %B %Y, %R %Z" - -formatCoinTime :: UTCTime -> Text -formatCoinTime = T.pack . formatTime defaultTimeLocale "%Y-%B-%e" diff --git a/frontend/src/Backend/Wallet.hs b/frontend/src/Backend/Wallet.hs index 8d5160d5..1e171e94 100755 --- a/frontend/src/Backend/Wallet.hs +++ b/frontend/src/Backend/Wallet.hs @@ -5,8 +5,8 @@ module Backend.Wallet where import Data.Maybe (fromMaybe) import Data.Text (Text) +import Common.Protocol (isMultiAssetOf) import Backend.Protocol.Types -import Backend.Utility (isMultiAssetOf) import CSL (TransactionUnspentOutputs) import qualified CSL import Config.Config (NetworkConfig (..), NetworkId (..), networkConfig) diff --git a/frontend/src/ENCOINS/Common/Events.hs b/frontend/src/Common/Events.hs similarity index 96% rename from frontend/src/ENCOINS/Common/Events.hs rename to frontend/src/Common/Events.hs index 96a6c85a..c9adecfb 100755 --- a/frontend/src/ENCOINS/Common/Events.hs +++ b/frontend/src/Common/Events.hs @@ -1,11 +1,11 @@ -module ENCOINS.Common.Events where +module Common.Events where import Control.Monad.IO.Class (liftIO) import Data.Text (Text) import Data.Time (NominalDiffTime) import Reflex.Dom -import Backend.Utility (toText) +import Common.Utility (toText) import JS.Website (logInfo) import Language.Javascript.JSaddle (MonadJSM, liftJSM, toJSVal, (#)) diff --git a/frontend/src/Backend/Protocol/Utility.hs b/frontend/src/Common/Protocol.hs old mode 100755 new mode 100644 similarity index 63% rename from frontend/src/Backend/Protocol/Utility.hs rename to frontend/src/Common/Protocol.hs index 72e0ea56..1caa41a2 --- a/frontend/src/Backend/Protocol/Utility.hs +++ b/frontend/src/Common/Protocol.hs @@ -1,4 +1,4 @@ -module Backend.Protocol.Utility where +module Common.Protocol where import Data.Bool (bool) import qualified Data.Map as Map @@ -8,10 +8,8 @@ import qualified Data.Text as Text import PlutusTx.Builtins import Text.Hex (decodeHex, encodeHex) -import Backend.Protocol.Fees (protocolFees) import Backend.Protocol.Setup ( bulletproofSetup - , emergentChangeAddress , encoinsCurrencySymbol , ledgerAddress ) @@ -34,47 +32,6 @@ getEncoinsInUtxos utxos = map MkAssetName $ Map.keys assets mapMaybe (CSL.multiasset . CSL.amount . CSL.output) utxos ) -mkWalletRedeemer :: - EncoinsMode - -> Address - -> Address - -> BulletproofParams - -> Secrets - -> [MintingPolarity] - -> Randomness - -> EncoinsRedeemer -mkWalletRedeemer mode ledgerAddr changeAddr bp secrets mps rs = red - where - (_, inputs, proof) = bulletproof bulletproofSetup bp secrets mps rs - v = calculateV secrets mps - inputs' = map (\(Input g p) -> (fromGroupElement g, p)) inputs - sig = - toBuiltin $ - fromJust $ - decodeHex "" - red = ((ledgerAddr, changeAddr, protocolFees mode v), (v, inputs'), proof, sig) - -mkLedgerRedeemer :: - EncoinsMode - -> Address - -> BulletproofParams - -> Secrets - -> [MintingPolarity] - -> Randomness - -> Address - -> Maybe EncoinsRedeemer -mkLedgerRedeemer mode ledgerAddr bp secrets mps rs changeAddr = - if changeAddr == emergentChangeAddress then Nothing else Just red - where - (_, inputs, proof) = bulletproof bulletproofSetup bp secrets mps rs - v = calculateV secrets mps - inputs' = map (\(Input g p) -> (fromGroupElement g, p)) inputs - sig = - toBuiltin $ - fromJust $ - decodeHex "" - red = ((ledgerAddr, changeAddr, protocolFees mode v), (v, inputs'), proof, sig) - verifyRedeemer :: BulletproofParams -> Maybe EncoinsRedeemer -> Bool verifyRedeemer bp (Just (_, (v, inputs), proof, _)) = verify bulletproofSetup bp v inputs' proof where @@ -113,3 +70,9 @@ hexToSecret = \case let n = byteStringToInteger $ toBuiltin bs (gamma, v) = n `divMod` (2 ^ (20 :: Integer)) return $ Secret (toFieldElement gamma) (toFieldElement v) + +isMultiAssetOf :: Text -> Text -> CSL.MultiAsset -> Bool +isMultiAssetOf symbol token (CSL.MultiAsset mp) = + case Map.lookup symbol mp of + Nothing -> False + Just i -> maybe False (const True) $ Map.lookup token i \ No newline at end of file diff --git a/frontend/src/Common/Reflex/Dom/Extra.hs b/frontend/src/Common/Reflex/Dom/Extra.hs new file mode 100644 index 00000000..895c1824 --- /dev/null +++ b/frontend/src/Common/Reflex/Dom/Extra.hs @@ -0,0 +1,11 @@ +module Common.Reflex.Dom.Extra where + +import Data.Text (Text) +import Reflex.Dom + +-- Element containing the result of a JavaScript computation +elementResultJS :: (MonadWidget t m) => Text -> (Text -> a) -> m (Dynamic t a) +elementResultJS resId f = + fmap (fmap f . value) $ + inputElement $ + def & initialAttributes .~ "style" =: "display:none;" <> "id" =: resId \ No newline at end of file diff --git a/frontend/src/Common/Reflex/Extra.hs b/frontend/src/Common/Reflex/Extra.hs new file mode 100644 index 00000000..566221a1 --- /dev/null +++ b/frontend/src/Common/Reflex/Extra.hs @@ -0,0 +1,81 @@ +module Common.Reflex.Extra where + +import Control.Monad (join, (<=<)) +import Data.Functor ((<&>)) +import Data.List ((\\)) +import Data.Maybe (isJust, isNothing) +import Data.Text (Text) +import Reflex.Dom +import Witherable (catMaybes) + +switchHoldDyn :: + (MonadWidget t m) => + Dynamic t a + -> (a -> m (Event t b)) + -> m (Event t b) +switchHoldDyn da f = switchHold never <=< dyn $ da <&> f + +dynHoldDyn :: + (MonadWidget t m) => + Dynamic t a + -> b + -> (a -> m (Dynamic t b)) + -> m (Dynamic t b) +dynHoldDyn dA b f = do + edB <- dyn $ f <$> dA + join <$> holdDyn (constDyn b) edB + +eventMaybe :: (Reflex t) => Event t (Maybe a) -> (Event t (), Event t a) +eventMaybe ev = (() <$ ffilter isNothing ev, catMaybes ev) + +eventMaybeDynDef :: + (Reflex t) => + Dynamic t def + -> Event t (Maybe a) + -> (Event t def, Event t a) +eventMaybeDynDef dDefault ev = (tagPromptlyDyn dDefault $ ffilter isNothing ev, catMaybes ev) + +eventTuple :: (Reflex t) => Event t (a, b) -> (Event t a, Event t b) +eventTuple ev = (fst <$> ev, snd <$> ev) + +dynTuple :: + (MonadWidget t m) => + a + -> b + -> Event t (a, b) + -> m (Dynamic t a, Dynamic t b) +dynTuple aDef bDef eAB = do + da <- holdDyn aDef $ fst <$> eAB + db <- holdDyn bDef $ snd <$> eAB + pure (da, db) + +foldDynamicAny :: (Reflex t) => [Dynamic t Bool] -> Dynamic t Bool +foldDynamicAny = foldr (zipDynWith (||)) (constDyn False) + +-- Fire 'Main event' only when there is some value in Condition event. +fireWhenJustThenReset :: + (MonadWidget t m) => + Event t a -- Main event + -> Event t (Maybe b) -- Condition event + -> Event t c -- Reset event + -> m (Event t ()) +fireWhenJustThenReset eMain eCondition eReset = do + -- Hold 'Main event' as True value , + -- and then after 'Reset event' fires + -- reset it to False. + dIsMain <- holdDyn False $ leftmost [True <$ eMain, False <$ eReset] + pure $ + attachPromptlyDynWithMaybe + (\isMain mCondition -> if isMain && isJust mCondition then Just () else Nothing) + dIsMain + eCondition + +updateUrls :: + (MonadWidget t m) => + Dynamic t [Text] + -> Event t (Maybe Text) + -> m (Dynamic t [Text]) +updateUrls dUrls eFailedUrl = do + dFailedUrls <- + foldDyn (\mUrl acc -> maybe acc (\u -> u : acc) mUrl) [] eFailedUrl + pure $ zipDynWith (\\) dUrls dFailedUrls diff --git a/frontend/src/ENCOINS/Common/Utils.hs b/frontend/src/Common/Url.hs similarity index 61% rename from frontend/src/ENCOINS/Common/Utils.hs rename to frontend/src/Common/Url.hs index d25de31d..1f598867 100755 --- a/frontend/src/ENCOINS/Common/Utils.hs +++ b/frontend/src/Common/Url.hs @@ -1,12 +1,9 @@ {-# LANGUAGE QuasiQuotes #-} -module ENCOINS.Common.Utils where +module Common.Url where -import Backend.Utility (toText) +import Config.Config (NetworkId (..), appNetwork) -import Control.Lens ((^.)) -import Control.Monad (guard) -import Data.Aeson (ToJSON, encode) import Data.Attoparsec.Text ( Parser , char @@ -17,22 +14,10 @@ import Data.Attoparsec.Text , () ) import qualified Data.Attoparsec.Text as A -import Data.ByteString (ByteString) -import Data.ByteString.Lazy (toStrict) import Data.Text (Text) import qualified Data.Text as T -import Data.Text.Encoding (decodeUtf8) import Data.Vector (Vector) import qualified Data.Vector as V -import qualified Foreign.JavaScript.Utils as Utils -import GHCJS.DOM.Blob -import qualified GHCJS.DOM.Document as D -import GHCJS.DOM.Element -import qualified GHCJS.DOM.HTMLElement as DOMHtml -import GHCJS.DOM.Types hiding (ByteString, Event, Text, toText) -import GHCJS.DOM.URL -import qualified Language.Javascript.JSaddle as JS -import Reflex.Dom import Text.RawString.QQ (r) import Text.Regex.TDFA ( CompOption (lastStarGreedy) @@ -44,19 +29,6 @@ import Text.Regex.TDFA ) import Text.Regex.TDFA.Text (compile) -toJsonText :: (ToJSON a) => a -> Text -toJsonText = decodeUtf8 . toJsonStrict - -toJsonStrict :: (ToJSON a) => a -> ByteString -toJsonStrict = toStrict . encode - -safeIndex :: [a] -> Int -> Maybe a -safeIndex zs n = guard (n >= 0) >> go zs n - where - go [] _ = Nothing - go (x : _) 0 = Just x - go (_ : xs) i = go xs (pred i) - checkUrl :: Text -> Bool checkUrl = regexPosixOpt urlRegexPosixPattern @@ -77,35 +49,6 @@ urlRegexPosixPattern :: Text urlRegexPosixPattern = [r|^https?://((25[0-5]|2[0-4][[:digit:]]|[01]?[[:digit:]][[:digit:]]?)\.(25[0-5]|2[0-4][[:digit:]]|[01]?[[:digit:]][[:digit:]]?)\.(25[0-5]|2[0-4][[:digit:]]|[01]?[[:digit:]][[:digit:]]?)\.(25[0-5]|2[0-4][[:digit:]]|[01]?[[:digit:]][[:digit:]]?)|(([[:alnum:]]+|([[:alnum:]]+\-[[:alnum:]]*)*[[:alnum:]])(\.([[:alnum:]]+|([[:alnum:]]+\-[[:alnum:]]*)*[[:alnum:]]))*\.([[:alpha:]]{2,})))/$|] -triggerDownload :: - (MonadJSM m) => - Document - -> Text - -- ^ mime type - -> Text - -- ^ file name - -> ByteString - -- ^ content - -> m () -triggerDownload doc mime filename s = do - t <- Utils.bsToArrayBuffer s - o <- JS.liftJSM $ JS.obj ^. JS.jss ("type" :: Text) (mime :: Text) - options <- JS.liftJSM $ BlobPropertyBag <$> JS.toJSVal o - blob <- newBlob [t] (Just options) - (url :: Text) <- createObjectURL blob - a <- D.createElement doc ("a" :: Text) - setAttribute a ("style" :: Text) ("display: none;" :: Text) - setAttribute a ("download" :: Text) filename - setAttribute a ("href" :: Text) url - DOMHtml.click $ DOMHtml.HTMLElement $ unElement a - revokeObjectURL url - -downloadVotes :: - (MonadWidget t m) => ByteString -> Text -> Int -> Event t () -> m () -downloadVotes txt name num e = do - doc <- askDocument - performEvent_ $ ffor e $ \_ -> - triggerDownload doc "application/json" (name <> toText num <> ".json") txt -------------------------------------------------------------------------------- -- Remove prefixes 'http(s):// and suffixes '/', ':' and further symbols rom URL @@ -156,3 +99,16 @@ parseHost = do in (fs, l V.! 0) (ns, c) = unsnoc xss' in pure (N $ NormalHost ns c) + +normalizePingUrl :: Text -> Text +normalizePingUrl url = T.append (T.dropWhileEnd (== '/') url) $ case appNetwork of + Mainnet -> + if T.isInfixOf "execute-api.eu-central-1.amazonaws.com" url + then "//" + else "/" + Testnet -> "/" + +normalizeCurrentUrl :: Text -> Text +normalizeCurrentUrl url = case url of + "localhost:3000" -> "http://localhost:3000" + u -> u \ No newline at end of file diff --git a/frontend/src/Common/Utility.hs b/frontend/src/Common/Utility.hs new file mode 100644 index 00000000..70ddc0e5 --- /dev/null +++ b/frontend/src/Common/Utility.hs @@ -0,0 +1,124 @@ +module Common.Utility where + +import Control.Monad (guard) +import qualified Crypto.Hash.Keccak as Keccak +import qualified Data.Aeson as A (ToJSON, encode) +import Data.Bool (bool) +import Data.ByteString (ByteString) +import qualified Data.ByteString.Base16 as BS16 +import Data.ByteString.Lazy (toStrict) +import Data.Foldable (foldl') +import qualified Data.Set as Set +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import Data.Time (UTCTime, defaultTimeLocale, formatTime) + + + +------------------------------------------------------------------------------- +-- List stuff +------------------------------------------------------------------------------- + +-- O(n * log(n)) instead of O(n^2) in 'nubBy' +-- It reverse list of tokens +nubWith :: (Ord b) => (a -> b) -> [a] -> [a] +nubWith f l = snd $ foldl' (func f) (Set.empty, []) l + where + func f' (set, acc) x + | Set.member x' set = (set, acc) + | otherwise = (Set.insert x' set, x : acc) + where + x' = f' x + +-- Combine two lists efficiently excluding duplicates from both lists +-- Original 'union' excludes duplicates from last list only. +-- O((n+m) * log(n+m)) instead of > O(n+m^2) +-- Give the priority to first list. +-- It reverse list of tokens +unionWith :: (Ord b) => (a -> b) -> [a] -> [a] -> [a] +unionWith f l1 l2 = + let func f' (set, acc) x + | Set.member x' set = (set, acc) + | otherwise = (Set.insert x' set, x : acc) + where + x' = f' x + listOfUniq = snd $ foldl' (func f) (Set.empty, []) $ l1 <> l2 + in listOfUniq + +safeIndex :: [a] -> Int -> Maybe a +safeIndex zs n = guard (n >= 0) >> go zs n + where + go [] _ = Nothing + go (x : _) 0 = Just x + go (_ : xs) i = go xs (pred i) + +-- it added to base from 4.15.0.0 +-- we are on base-4.12.0.0 +singletonL :: a -> [a] +singletonL x = [x] + +------------------------------------------------------------------------------- +-- Text stuff +------------------------------------------------------------------------------- + +toEither :: e -> Maybe a -> Either e a +toEither err Nothing = Left err +toEither _ (Just a) = Right a + +textMaybe :: Text -> Maybe Text +textMaybe txt = bool (Just txt) Nothing $ T.null txt + +space :: Text +space = " " + +column :: Text +column = ":" + +toText :: (Show a) => a -> Text +toText = T.pack . show + +------------------------------------------------------------------------------- +-- Hash stuff +------------------------------------------------------------------------------- + +data HashBit = B512 | B384 | B256 | B224 + deriving (Eq) + +hashKeccak :: HashBit -> Text -> Text +hashKeccak hb raw = + let keccak = case hb of + B512 -> Keccak.keccak512 + B384 -> Keccak.keccak384 + B256 -> Keccak.keccak256 + B224 -> Keccak.keccak224 + in TE.decodeUtf8 $ BS16.encode $ keccak $ TE.encodeUtf8 raw + +hashKeccak512 :: Text -> Text +hashKeccak512 = hashKeccak B512 + +hashKeccak256 :: Text -> Text +hashKeccak256 = hashKeccak B256 + +isHashOfRaw :: Text -> Text -> Bool +isHashOfRaw hash raw = hash == hashKeccak512 raw + +------------------------------------------------------------------------------- +-- Time stuff +------------------------------------------------------------------------------- + +formatPollTime :: UTCTime -> Text +formatPollTime = T.pack . formatTime defaultTimeLocale "%e %B %Y, %R %Z" + +formatCoinTime :: UTCTime -> Text +formatCoinTime = T.pack . formatTime defaultTimeLocale "%Y-%B-%e" + +------------------------------------------------------------------------------- +-- JSON stuff +------------------------------------------------------------------------------- + +toJsonText :: (A.ToJSON a) => a -> Text +toJsonText = TE.decodeUtf8 . toJsonStrict + +toJsonStrict :: (A.ToJSON a) => a -> ByteString +toJsonStrict = toStrict . A.encode diff --git a/frontend/src/ENCOINS/App/Body.hs b/frontend/src/ENCOINS/App/Body.hs index b93194af..dd3f32bd 100755 --- a/frontend/src/ENCOINS/App/Body.hs +++ b/frontend/src/ENCOINS/App/Body.hs @@ -10,15 +10,12 @@ import Reflex.Dom import Backend.Protocol.StrongTypes (toPasswordHash) import Backend.Protocol.Types (PasswordRaw (..)) -import Backend.Utility (switchHoldDyn) -import Backend.Status (AppStatus(..)) -import Backend.Wallet (walletsSupportedInApp, Wallet(walletName)) -import ENCOINS.App.Widgets.Basic - ( loadAppDataE - , waitForScripts - ) +import Backend.Status (AppStatus (..)) +import Backend.Wallet (Wallet (walletName), walletsSupportedInApp) +import Common.Events +import Common.Reflex.Extra (switchHoldDyn) import ENCOINS.App.Widgets.CloudWindow (cloudSettingsWindow) -import ENCOINS.App.Widgets.ConnectWindow (connectWindow) +import ENCOINS.Common.ConnectWindow (connectWindow) import ENCOINS.App.Widgets.MainWindow (mainWindow) import ENCOINS.App.Widgets.Navbar (navbarWidget) import ENCOINS.App.Widgets.Notification @@ -35,10 +32,10 @@ import ENCOINS.App.Widgets.WelcomeWindow import ENCOINS.Common.Cache ( aesKey , isCloudOn + , loadAppDataE , passwordStorageKey ) -import ENCOINS.Common.Events -import ENCOINS.Common.Widgets.Advanced (copiedNotification) +import ENCOINS.Common.Widgets.Advanced (viewCopiedNotification, waitForScripts) import ENCOINS.Common.Widgets.Basic (notification) import ENCOINS.Common.Widgets.JQuery (jQueryWidget) import ENCOINS.Common.Widgets.MoreMenu @@ -108,7 +105,7 @@ bodyContentWidget mPass = mdo -- re-encrypt current cache with new pass reEncryptCurrentCache dTokensV3 dmKey eReEncrypt - copiedNotification + viewCopiedNotification dSaveOnFromCache <- loadAppDataE Nothing isCloudOn "app-body-load-is-save-on-key" id False diff --git a/frontend/src/ENCOINS/App/Widgets/Basic.hs b/frontend/src/ENCOINS/App/Widgets/Basic.hs deleted file mode 100755 index 81d0c977..00000000 --- a/frontend/src/ENCOINS/App/Widgets/Basic.hs +++ /dev/null @@ -1,164 +0,0 @@ -module ENCOINS.App.Widgets.Basic where - -import Backend.Protocol.Types (PasswordRaw (..)) -import Backend.Status - ( AppStatus (..) - , WalletStatus (..) - ) -import ENCOINS.Common.Events -import ENCOINS.Common.Utils (toJsonText) -import JS.Website (loadJSON, removeKey, saveJSON) - -import Control.Monad (void) -import Data.Aeson (FromJSON, ToJSON, decode, decodeStrict) -import Data.ByteString (ByteString) -import Data.ByteString.Lazy (fromStrict) -import Data.Text (Text) -import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) -import GHCJS.DOM (currentWindowUnchecked) -import GHCJS.DOM.Storage (getItem, setItem) -import GHCJS.DOM.Types (MonadDOM) -import GHCJS.DOM.Window (getLocalStorage) -import Reflex.Dom -import Reflex.ScriptDependent (widgetHoldUntilDefined) - -sectionApp :: (MonadWidget t m) => Text -> Text -> m a -> m a -sectionApp elemId cls = - elAttr - "div" - ("id" =: elemId <> "class" =: "section-app wf-section " `T.append` cls) - -containerApp :: (MonadWidget t m) => Text -> m a -> m a -containerApp cls = divClass ("container-app w-container " `T.append` cls) - --- Element containing the result of a JavaScript computation -elementResultJS :: (MonadWidget t m) => Text -> (Text -> a) -> m (Dynamic t a) -elementResultJS resId f = - fmap (fmap f . value) $ - inputElement $ - def & initialAttributes .~ "style" =: "display:none;" <> "id" =: resId - -waitForScripts :: (MonadWidget t m) => m () -> m () -> m () -waitForScripts placeholderWidget actualWidget = do - ePB <- getPostBuild - _ <- - widgetHoldUntilDefined - "walletAPI" - ("js/ENCOINS.js" <$ ePB) - placeholderWidget - actualWidget - blank - -loadAppDataE :: - forall t m a b. - (MonadWidget t m, FromJSON a, Show a, Show b) => - Maybe PasswordRaw - -> Text -- cache key - -> Text -- response id - -> (a -> b) - -> b - -> m (Dynamic t b) -loadAppDataE mPass key resId f val = do - e <- newEventWithDelay 0.1 - loadAppData mPass key resId e f val - -loadAppData :: - forall t m a b. - (MonadWidget t m, FromJSON a, Show a, Show b) => - Maybe PasswordRaw - -> Text -- cache key - -> Text -- response id - -> Event t () - -> (a -> b) - -> b - -> m (Dynamic t b) -loadAppData mPass key resId ev f val = do - dmRes <- loadAppDataM mPass key resId ev - let dRes = maybe val f <$> dmRes - pure dRes - -loadAppDataME :: - forall t m a. - (MonadWidget t m, FromJSON a, Show a) => - Maybe PasswordRaw - -> Text -- cache key - -> Text -- response id - -> m (Dynamic t (Maybe a)) -loadAppDataME mPass key resId = do - e <- newEventWithDelay 0.1 - loadAppDataM mPass key resId e - -loadAppDataM :: - forall t m a. - (MonadWidget t m, FromJSON a, Show a) => - Maybe PasswordRaw - -> Text -- cache key - -> Text -- response id - -> Event t () - -> m (Dynamic t (Maybe a)) -loadAppDataM mPass key resId ev = do - let mPassT = (getPassRaw <$> mPass) - performEvent_ (loadJSON key resId mPassT <$ ev) - dRes <- - elementResultJS resId ((decodeStrict :: ByteString -> Maybe a) . encodeUtf8) - pure dRes - -saveAppData_ :: - (MonadWidget t m, ToJSON a) => - Maybe PasswordRaw - -> Text - -> Event t a - -> m () -saveAppData_ mPass key eVal = do - void $ saveAppData mPass key eVal - -saveAppData :: - (MonadWidget t m, ToJSON a) => - Maybe PasswordRaw - -> Text - -> Event t a - -> m (Event t ()) -saveAppData mPass key eVal = do - let eEncodedValue = toJsonText <$> eVal - let mPassT = (getPassRaw <$> mPass) - performEvent (saveJSON mPassT key <$> eEncodedValue) - -removeCacheKey :: - (MonadWidget t m) => - Event t Text - -> m (Event t ()) -removeCacheKey eKey = performEvent (removeKey <$> eKey) - -loadJsonFromStorage :: (MonadDOM m, FromJSON a) => Text -> m (Maybe a) -loadJsonFromStorage elId = do - lc <- currentWindowUnchecked >>= getLocalStorage - (>>= decode . fromStrict . encodeUtf8) <$> getItem lc elId - -saveJsonToStorage :: (MonadDOM m, ToJSON a) => Text -> a -> m () -saveJsonToStorage elId val = do - lc <- currentWindowUnchecked >>= getLocalStorage - setItem lc elId . toJsonText $ val - -loadTextFromStorage :: (MonadDOM m) => Text -> m (Maybe Text) -loadTextFromStorage key = do - lc <- currentWindowUnchecked >>= getLocalStorage - getItem lc key - --- Wallet error element -walletError :: (MonadWidget t m) => m (Event t WalletStatus) -walletError = do - dWalletError <- elementResultJS "walletErrorElement" id - let eWalletError = ffilter ("" /=) $ updated dWalletError - return $ WalletFail <$> eWalletError - -tellAppStatus :: - (MonadWidget t m, EventWriter t [AppStatus] m) => - Event t AppStatus - -> m () -tellAppStatus ev = tellEvent $ singletonL <$> ev - --- it added to base from 4.15.0.0 --- we are on base-4.12.0.0 -singletonL :: a -> [a] -singletonL x = [x] diff --git a/frontend/src/ENCOINS/App/Widgets/Cloud.hs b/frontend/src/ENCOINS/App/Widgets/Cloud.hs index ffd1f56c..5cb425b4 100644 --- a/frontend/src/ENCOINS/App/Widgets/Cloud.hs +++ b/frontend/src/ENCOINS/App/Widgets/Cloud.hs @@ -4,24 +4,25 @@ module ENCOINS.App.Widgets.Cloud where import Backend.Protocol.Types -import Backend.Wallet(WalletName, toJS) import Backend.Servant.Requests (restoreRequest, savePingRequest, saveRequest) import Backend.Status ( AppStatus (..) , CloudIconStatus (..) , CloudRestoreStatus (..) ) -import Backend.Utility (eventMaybeDynDef, switchHoldDyn, toText, unionWith, hashKeccak256) -import ENCOINS.App.Widgets.Basic - ( elementResultJS - , loadAppData - , saveAppData - , tellAppStatus +import Backend.Wallet (WalletName, toJS) +import Common.Events +import Common.Reflex.Dom.Extra (elementResultJS) +import Common.Reflex.Extra (eventMaybeDynDef, switchHoldDyn) +import Common.Utility + ( hashKeccak256 + , toJsonStrict + , toText + , unionWith ) import ENCOINS.Bulletproofs (Secret (..)) -import ENCOINS.Common.Cache (aesKey) -import ENCOINS.Common.Events -import ENCOINS.Common.Utils (toJsonStrict) +import ENCOINS.Common.Cache (aesKey, loadAppData, saveAppData) +import ENCOINS.Common.Widgets.Advanced (tellAppStatus) import ENCOINS.Crypto.Field (Field (F)) import qualified JS.App as JS @@ -287,10 +288,13 @@ makeSignedKey :: makeSignedKey mPass dWalletName ev = do let getKeyElId = "getKeyFromSign2" ev2 <- delay 0.1 ev - performEvent_ $ JS.getSignedKey getKeyElId <$> tagPromptlyDyn (toJS <$> dWalletName) ev2 - eSignedKey <- updated <$> elementResultJS - getKeyElId - id + performEvent_ $ + JS.getSignedKey getKeyElId <$> tagPromptlyDyn (toJS <$> dWalletName) ev2 + eSignedKey <- + updated + <$> elementResultJS + getKeyElId + id let eHashedSign = hashKeccak256 <$> eSignedKey let eAesKey = MkAesKeyRaw <$> eHashedSign eKeySaved <- saveAppData mPass aesKey eAesKey diff --git a/frontend/src/ENCOINS/App/Widgets/CloudWindow.hs b/frontend/src/ENCOINS/App/Widgets/CloudWindow.hs index f08b2d4e..9427654c 100644 --- a/frontend/src/ENCOINS/App/Widgets/CloudWindow.hs +++ b/frontend/src/ENCOINS/App/Widgets/CloudWindow.hs @@ -5,13 +5,17 @@ module ENCOINS.App.Widgets.CloudWindow where import Backend.Protocol.Types import Backend.Status (CloudIconStatus (..)) -import Backend.Utility (space, switchHoldDyn) import Backend.Wallet (WalletName (..)) -import ENCOINS.App.Widgets.Basic (removeCacheKey, saveAppData, saveAppData_) +import Common.Events +import Common.Reflex.Extra (switchHoldDyn) +import Common.Utility (space) import ENCOINS.App.Widgets.Cloud (fetchAesKey, genAesKey, makeSignedKey) -import ENCOINS.Common.Cache (aesKey, isCloudOn) -import ENCOINS.Common.Events -import ENCOINS.Common.Widgets.Advanced (copyButton, dialogWindow, withTooltip) +import ENCOINS.Common.Cache (aesKey, isCloudOn, removeCacheKey, saveAppData, saveAppData_) +import ENCOINS.Common.Widgets.Advanced + ( dialogWindow + , viewCopyButton + , withTooltip + ) import ENCOINS.Common.Widgets.Basic ( br , btn @@ -59,7 +63,7 @@ cloudSettingsWindow mPass dWalletName cloudCacheFlag dCloudStatus eOpen = mdo dmNewKey <- cloudKeyWidget mPass dWalletName eFirstKeyLoad divClass "app-Cloud_Restore_Title" $ text "Restore all unburned encoins from cloud with your current key" - eRestore <- restoreButton dmNewKey + eRestore <- viewRestoreButton dmNewKey pure $ align (updated dmNewKey) eRestore dmNewKey <- holdDyn Nothing emNewKey pure (dIsCloudOn, dmNewKey, eRestore) @@ -71,7 +75,7 @@ cloudCheckbox :: -> m (Dynamic t Bool, Event t Bool) cloudCheckbox cloudCacheFlag = do (dIsChecked, eCloudChange) <- - checkboxWidget (updated cloudCacheFlag) "app-Cloud_CheckboxToggle" + viewCheckbox (updated cloudCacheFlag) "app-Cloud_CheckboxToggle" saveAppData_ Nothing isCloudOn $ updated dIsChecked pure (dIsChecked, eCloudChange) @@ -97,12 +101,12 @@ selectSaveStatusNote status isCloud = (FailedSave, _) -> "failed" in "The synchronization" <> space <> t -checkboxWidget :: +viewCheckbox :: (MonadWidget t m) => Event t Bool -> Text -> m (Dynamic t Bool, Event t Bool) -checkboxWidget initial checkBoxClass = divClass "w-row app-Cloud_CheckboxContainer" $ do +viewCheckbox initial checkBoxClass = divClass "w-row app-Cloud_CheckboxContainer" $ do inp <- inputElement $ def @@ -124,7 +128,7 @@ showKeyWidget dmKey = do let keyIcon = do void $ image "info-black.svg" "app-Cloud_IconPopup" "" let copyIcon = do - e <- copyButton + e <- viewCopyButton let eKey = tagPromptlyDyn dKey e performEvent_ (liftIO . copyText <$> eKey) divClass "app-Cloud_KeyContainer" $ do @@ -134,11 +138,11 @@ showKeyWidget dmKey = do "Tip: store it offline and protect with a password / encryption. Enable password protection in the Encoins app." dynText dKey -restoreButton :: +viewRestoreButton :: (MonadWidget t m) => Dynamic t (Maybe AesKeyRaw) -> m (Event t ()) -restoreButton dmKey = +viewRestoreButton dmKey = divClass "app-Cloud_Restore_ButtonContainer" $ btnWithBlock "button-switching inverted flex-center" "" (isNothing <$> dmKey) $ text "Restore" @@ -160,7 +164,7 @@ cloudKeyWidget mPass dWalletName eFirstLoadKey = mdo let dmCorrectKey = checkUserKeyValid <$> dInputCloudKey let dBorderLine = zipDynWith selectBorderColor dmKey dmCorrectKey - dInputCloudKey <- inputCloudKeyWidget dBorderLine eFirstLoadKey + dInputCloudKey <- viewInputCloudKey dBorderLine eFirstLoadKey let eKeyInputByUser = attachPromptlyDynWithMaybe const dmCorrectKey eEnter eUserKeySaved <- saveAppData mPass aesKey eKeyInputByUser @@ -218,12 +222,12 @@ cloudKeyWidget mPass dWalletName eFirstLoadKey = mdo divClass "app-Cloud_ButtonDescription" $ dynText dButtonDescription pure dmKey -inputCloudKeyWidget :: +viewInputCloudKey :: (MonadWidget t m) => Dynamic t Text -> Event t () -> m (Dynamic t Text) -inputCloudKeyWidget dBorder eOpen = divClass "w-row" $ do +viewInputCloudKey dBorder eOpen = divClass "w-row" $ do inp <- inputElement $ def @@ -266,7 +270,7 @@ deleteKeyDialog eDelete = mdo "This action will remove cloud key from the cache! If you won't remember the key you can't recover encoins from remote server!" br text "Are you sure?" - elAttr "div" ("class" =: "app-columns w-row app-DeleteKey_ButtonContainer") $ do + elAttr "div" ("class" =: "w-row app-DeleteKey_ButtonContainer") $ do btnOk <- btn "button-switching inverted flex-center" "" $ text "Delete" btnCancel <- btn "button-switching flex-center" "" $ text "Cancel" return (btnOk, btnCancel) diff --git a/frontend/src/ENCOINS/App/Widgets/Coin.hs b/frontend/src/ENCOINS/App/Widgets/Coin.hs index af6fac03..624ee091 100755 --- a/frontend/src/ENCOINS/App/Widgets/Coin.hs +++ b/frontend/src/ENCOINS/App/Widgets/Coin.hs @@ -21,13 +21,13 @@ import Backend.Protocol.Types , SaveStatus (..) , TokenCacheV3 (..) ) -import Backend.Protocol.Utility (secretToHex) -import Backend.Utility (toText) +import Common.Protocol (secretToHex) +import Common.Utility (toText) import ENCOINS.BaseTypes (FieldElement) import ENCOINS.Bulletproofs (Secret (..), Secrets, fromSecret) import ENCOINS.Common.Widgets.Advanced - ( checkboxButton - , copyButton + ( viewCheckboxButton + , viewCopyButton , copyEvent , withTooltip ) @@ -118,7 +118,7 @@ coinBurnWidget :: -> m (Dynamic t (Maybe (Secret, TokenCacheV3))) coinBurnWidget tokenV3@(MkTokenCacheV3 name s _) = mdo (elTxt, ret) <- elDynAttr "div" (mkAttrs <$> dIsSpoilerVisible) $ do - dChecked <- divClass "" checkboxButton + dChecked <- divClass "" viewCheckboxButton (txt, _) <- elClass' "div" "app-text-normal" $ do text $ shortenCoinName $ getAssetName name let arrowClass = @@ -135,7 +135,7 @@ coinBurnWidget tokenV3@(MkTokenCacheV3 name s _) = mdo divClass "key-div" $ withTooltip keyIcon "app-CoinBurn_KeyTip" 0 0 $ do divClass "app-text-semibold" $ text "Minting Key" divClass "app-ToolTip_MintingKey" $ do - e <- copyButton + e <- viewCopyButton performEvent_ (liftIO (copyText secretText) <$ e) text $ " " <> secretText return (txt, dChecked) @@ -177,7 +177,7 @@ coinSpoiler (MkAssetName name) = elAttr ) $ do let copyTokenIcon = do - eCopy <- copyButton + eCopy <- viewCopyButton performEvent_ (liftIO (copyText name) <$ eCopy) divClass "app-text-semibold" $ text "Full token name" divClass "app-Tooltip_TokenNameContainer" $ do @@ -191,7 +191,7 @@ coinSpoiler (MkAssetName name) = elAttr fp <- fingerprintFromAssetName encoinsCurrencySymbol name let copyAssetIcon = do - eCopy <- copyButton + eCopy <- viewCopyButton performEvent_ (liftIO (copyText fp) <$ eCopy) divClass "app-text-semibold" $ text "Asset fingerprint" divClass "app-Tooltip_AssetContainer" $ do diff --git a/frontend/src/ENCOINS/App/Widgets/ConnectWindow.hs b/frontend/src/ENCOINS/App/Widgets/ConnectWindow.hs deleted file mode 100755 index e5651b62..00000000 --- a/frontend/src/ENCOINS/App/Widgets/ConnectWindow.hs +++ /dev/null @@ -1,60 +0,0 @@ -{-# LANGUAGE RecursiveDo #-} - -module ENCOINS.App.Widgets.ConnectWindow - ( connectWindow - ) where - -import Control.Monad (void) -import Data.Bool (bool) -import Reflex.Dom - -import Backend.Utility (toText) -import Backend.Wallet (Wallet (..), WalletName (..), fromJS, toJS) -import ENCOINS.App.Widgets.Basic (loadAppDataE, saveAppData_) -import ENCOINS.Common.Cache (currentWallet) -import ENCOINS.Common.Widgets.Advanced (dialogWindow) -import ENCOINS.Common.Widgets.Wallet (loadWallet, walletIcon) - -walletEntry :: (MonadWidget t m) => WalletName -> m (Event t WalletName) -walletEntry w = do - (e, _) <- elAttr' "div" ("class" =: "connect-wallet-div") $ do - divClass "app-text-normal" $ text $ bool "Disconnect" (toText w) $ w /= None - elAttr - "a" - ( "href" =: "#" - <> "class" =: "w-inline-block" - <> "style" =: "margin-left:150px;" - ) - $ bool blank (walletIcon w) - $ w /= None - return (w <$ domEvent Click e) - -connectWindow :: - (MonadWidget t m) => [WalletName] -> Event t () -> m (Dynamic t Wallet) -connectWindow supportedWallets eConnectOpen = mdo - (eConnectClose, dWallet) <- dialogWindow - True - eConnectOpen - eConnectClose - "common-ConnectWindow" - "Connect Wallet" $ mdo - eWalletName <- - divClass "common-Connect_WalletContainer" $ - leftmost . ([eLastWalletName] ++) <$> mapM walletEntry supportedWallets - eUpdate <- tag bWalletName <$> tickLossyFromPostBuildTime 10 - dW <- loadWallet (leftmost [eWalletName, eUpdate]) >>= holdUniqDyn - let bWalletName = current $ fmap walletName dW - - -- save/load wallet - saveAppData_ Nothing currentWallet $ toJS <$> eWalletName - eLastWalletName <- - updated - <$> loadAppDataE - Nothing - currentWallet - "connectWindow-key-currentWallet" - fromJS - None - - return (void eWalletName, dW) - return dWallet diff --git a/frontend/src/ENCOINS/App/Widgets/ImportWindow.hs b/frontend/src/ENCOINS/App/Widgets/ImportWindow.hs index 662ede40..68c5733d 100755 --- a/frontend/src/ENCOINS/App/Widgets/ImportWindow.hs +++ b/frontend/src/ENCOINS/App/Widgets/ImportWindow.hs @@ -21,11 +21,11 @@ import JS.Website (saveTextFile) import Reflex.Dom import Witherable (catMaybes) -import Backend.Protocol.Utility (hexToSecret) -import Backend.Utility (formatCoinTime, switchHoldDyn) +import Common.Protocol (hexToSecret) +import Common.Reflex.Extra (switchHoldDyn) +import Common.Utility (formatCoinTime, toJsonText) import ENCOINS.Bulletproofs (Secret) -import ENCOINS.Common.Events -import ENCOINS.Common.Utils (toJsonText) +import Common.Events import ENCOINS.Common.Widgets.Advanced (dialogWindow) import ENCOINS.Common.Widgets.Basic (btn, btnWithBlock) @@ -82,22 +82,24 @@ selectBorderColor mSecret origInput = else maybe "border-color: #ff3e31;" (const "border-color: #00cb7a;") mSecret importCoinFiles :: (MonadWidget t m) => Event t () -> m (Event t [Secret]) -importCoinFiles eImportOpen = divClass "app-ImportFile_Container" $ mdo - divClass "app-ImportWindow_SubTitle" $ text "Choose a file to import coins:" - let conf = - def{_inputElementConfig_setValue = pure ("" <$ eImportOpen)} - & (initialAttributes .~ ("class" =: "app-ImportFile_Input" <> "type" =: "file")) - (eImportClose, dResult) <- divClass "app-ImportFile_Container_InputAndButton" $ do - dFiles <- _inputElement_files <$> inputElement conf - emFileContent <- switchHoldDyn dFiles $ \case - [file] -> readFileContent file - _ -> pure never - dContent <- holdDyn "" (catMaybes emFileContent) - let parseContent = fromMaybe [] . decode . fromStrict . encodeUtf8 - let dRes = parseContent <$> dContent - eClose <- divClass "app-ImportFile_ButtonContainer" $ do - btn "button-switching inverted flex-center app-ImportFile_Button" "" $ text "Ok" - pure (eClose, dRes) +importCoinFiles eImportOpen = do + (eImportClose, dInputFiles) <- divClass "app-ImportFile_Container" $ do + divClass "app-ImportWindow_SubTitle" $ text "Choose a file to import coins:" + let conf = + def{_inputElementConfig_setValue = pure ("" <$ eImportOpen)} + & (initialAttributes .~ ("class" =: "app-ImportFile_Input" <> "type" =: "file")) + divClass "app-ImportFile_Container_InputAndButton" $ do + dFiles <- _inputElement_files <$> inputElement conf + eClose <- divClass "app-ImportFile_ButtonContainer" $ do + btn "button-switching inverted flex-center app-ImportFile_Button" "" $ text "Ok" + pure (eClose, dFiles) + + emFileContent <- switchHoldDyn dInputFiles $ \case + [file] -> readFileContent file + _ -> pure never + dContent <- holdDyn "" (catMaybes emFileContent) + let parseContent = fromMaybe [] . decode . fromStrict . encodeUtf8 + let dResult = parseContent <$> dContent return (current dResult `tag` eImportClose) readFileContent :: (MonadWidget t m) => File -> m (Event t (Maybe Text)) diff --git a/frontend/src/ENCOINS/App/Widgets/InputAddressWindow.hs b/frontend/src/ENCOINS/App/Widgets/InputAddressWindow.hs index 5bcc01fc..8a1103b1 100755 --- a/frontend/src/ENCOINS/App/Widgets/InputAddressWindow.hs +++ b/frontend/src/ENCOINS/App/Widgets/InputAddressWindow.hs @@ -10,8 +10,8 @@ import Witherable (catMaybes) import Backend.Protocol.Types import Config.Config (NetworkConfig (..), NetworkId (..), networkConfig) -import ENCOINS.App.Widgets.Basic (elementResultJS) -import ENCOINS.Common.Events +import Common.Reflex.Dom.Extra (elementResultJS) +import Common.Events import ENCOINS.Common.Widgets.Advanced (dialogWindow) import ENCOINS.Common.Widgets.Basic (btnWithBlock, errDiv) import JS.App (addrLoad) @@ -23,7 +23,7 @@ inputAddressWindow eOpen = mdo divClass "connect-title-div" $ divClass "app-text-semibold" $ text "Enter wallet address in bech32:" - dAddrInp <- divClass "app-columns w-row" $ do + dAddrInp <- divClass "w-row" $ do inp <- inputElement $ def @@ -63,7 +63,7 @@ inputAddressWindow eOpen = mdo err = elAttr "div" - ( "class" =: "app-columns w-row" + ( "class" =: "w-row" <> "style" =: "display:flex;justify-content:center;" ) $ errDiv "Incorrect address" diff --git a/frontend/src/ENCOINS/App/Widgets/MainTabs.hs b/frontend/src/ENCOINS/App/Widgets/MainTabs.hs index 181e063d..f480f6ae 100755 --- a/frontend/src/ENCOINS/App/Widgets/MainTabs.hs +++ b/frontend/src/ENCOINS/App/Widgets/MainTabs.hs @@ -24,17 +24,13 @@ import Backend.Status , WalletTxStatus (..) , isAppTotalBlock ) -import Backend.Utility (nubWith) import Backend.Wallet (Wallet (..)) -import Config.Config (delegateServerUrl) -import ENCOINS.App.Widgets.Basic - ( containerApp - , elementResultJS - , saveAppData - , sectionApp - , tellAppStatus - , walletError +import Common.Events +import Common.Reflex.Dom.Extra + ( elementResultJS ) +import Common.Utility (nubWith) +import Config.Config (delegateServerUrl) import ENCOINS.App.Widgets.Cloud import ENCOINS.App.Widgets.Coin ( CoinUpdate (..) @@ -64,9 +60,12 @@ import ENCOINS.App.Widgets.WelcomeWindow , welcomeWindowLedgerStorageKey , welcomeWindowTransferStorageKey ) -import ENCOINS.Common.Cache (encoinsV3) -import ENCOINS.Common.Events -import ENCOINS.Common.Widgets.Basic (btn, divClassId) +import ENCOINS.Common.Cache (encoinsV3, saveAppData) +import ENCOINS.Common.Widgets.Advanced + ( tellAppStatus + , walletError + ) +import ENCOINS.Common.Widgets.Basic (btn, containerApp, divClassId, sectionApp) mainWindowColumnHeader :: (MonadWidget t m) => Text -> m () mainWindowColumnHeader title = @@ -106,7 +105,7 @@ walletTab mpass dWallet dTokenCacheOld dCloudOn dmKey eWasMigration = sectionApp 0 containerApp "" $ transactionBalanceWidget formula (Just WalletMode) "" (dToBurn, dToMint, eStatusUpdate, dNewTokensV3) <- containerApp "" $ - divClass "app-columns w-row" $ mdo + divClass "w-row" $ mdo dImportedSecrets <- foldDyn (++) [] eImportSecret dNewSecrets <- foldDyn (++) [] $ tagPromptlyDyn dCoinsToMint eSend let dTokenCache = @@ -132,7 +131,7 @@ walletTab mpass dWallet dTokenCacheOld dCloudOn dmKey eWasMigration = sectionApp coinBurnCollectionWidget dSecretsUniq eImp <- divClassId "" "welcome-import-export" $ do (eImport, eExport) <- - divClass "app-columns w-row" $ + divClass "w-row" $ (,) <$> menuButton "Import" <*> menuButton "Export" exportWindow eExport dCTB (map tcSecret <$> dTokenCache) (eIS, eISAll) <- importWindow eImport @@ -223,7 +222,7 @@ transferTab mpass dWallet dTokenCacheOld dCloudOn dmKey eWasMigration = sectionA let formula = Formula dDepositBalance 0 0 0 (getCoinNumber <$> dCoins) 0 containerApp "" $ transactionBalanceWidget formula (Just TransferMode) " (to Ledger)" - (dCoins, eSendToLedger, eAddr, dTokensV3) <- containerApp "" $ divClass "app-columns w-row" $ mdo + (dCoins, eSendToLedger, eAddr, dTokensV3) <- containerApp "" $ divClass "w-row" $ mdo dImportedSecrets <- foldDyn (++) [] eImportSecret let dTokenCache = nubWith tcAssetName @@ -244,7 +243,7 @@ transferTab mpass dWallet dTokenCacheOld dCloudOn dmKey eWasMigration = sectionA dyn_ $ fmap noCoinsFoundWidget dSecretsInTheWallet coinBurnCollectionWidget dSecretsInTheWallet (eImport, eExport) <- - divClass "app-columns w-row" $ + divClass "w-row" $ (,) <$> menuButton "Import" <*> menuButton "Export" exportWindow eExport dCTB (map tcSecret <$> dTokenCache) (eIS, eISAll) <- importWindow eImport @@ -358,7 +357,7 @@ ledgerTab mpass dTokenCacheOld dCloudOn dmKey eWasMigration = sectionApp "" "" $ containerApp "" $ transactionBalanceWidget formula (Just LedgerMode) "" (dToBurn, dToMint, dAddr, eStatusUpdate, dNewTokensV3) <- containerApp "" $ - divClassId "app-columns w-row" "welcome-ledger" $ mdo + divClassId "w-row" "welcome-ledger" $ mdo dImportedSecrets <- foldDyn (++) [] eImportSecret dNewSecrets <- foldDyn (++) [] $ tagPromptlyDyn dCoinsToMint eSend let dTokenCache = @@ -382,7 +381,7 @@ ledgerTab mpass dTokenCacheOld dCloudOn dmKey eWasMigration = sectionApp "" "" $ coinBurnCollectionWidget dSecretsUniq eImp <- divClass "" $ do (eImport, eExport) <- - divClass "app-columns w-row" $ + divClass "w-row" $ (,) <$> menuButton "Import" <*> menuButton "Export" exportWindow eExport dCTB (map tcSecret <$> dTokenCache) (eIS, eISAll) <- importWindow eImport diff --git a/frontend/src/ENCOINS/App/Widgets/MainWindow.hs b/frontend/src/ENCOINS/App/Widgets/MainWindow.hs index 6f3c8cb7..a071b659 100755 --- a/frontend/src/ENCOINS/App/Widgets/MainWindow.hs +++ b/frontend/src/ENCOINS/App/Widgets/MainWindow.hs @@ -8,9 +8,10 @@ import Reflex.Dom import Backend.Protocol.Types import Backend.Status (AppStatus) -import Backend.Utility (switchHoldDyn, unionWith) import Backend.Wallet (Wallet (..)) -import ENCOINS.App.Widgets.Basic (loadAppDataME) +import Common.Events +import Common.Reflex.Extra (switchHoldDyn) +import Common.Utility (unionWith) import ENCOINS.App.Widgets.Cloud ( resetTokens , restoreValidTokens @@ -18,8 +19,7 @@ import ENCOINS.App.Widgets.Cloud import ENCOINS.App.Widgets.MainTabs (ledgerTab, transferTab, walletTab) import ENCOINS.App.Widgets.Migration (migrateTokenCacheV3) import ENCOINS.App.Widgets.TabsSelection (AppTab (..), tabsSection) -import ENCOINS.Common.Cache (encoinsV3) -import ENCOINS.Common.Events +import ENCOINS.Common.Cache (encoinsV3, loadAppDataME) mainWindow :: (MonadWidget t m, EventWriter t [AppStatus] m) => diff --git a/frontend/src/ENCOINS/App/Widgets/Migration.hs b/frontend/src/ENCOINS/App/Widgets/Migration.hs index db6abf06..d9b5eb42 100644 --- a/frontend/src/ENCOINS/App/Widgets/Migration.hs +++ b/frontend/src/ENCOINS/App/Widgets/Migration.hs @@ -8,17 +8,12 @@ import Reflex.Dom import Backend.Protocol.Types (PasswordRaw (..), TokenCacheV3) import Backend.Status (AppStatus (..), MigrateStatus (..)) -import Backend.Utility (switchHoldDyn) -import ENCOINS.App.Widgets.Basic - ( loadAppData - , saveAppData - , tellAppStatus - ) +import Common.Events +import Common.Reflex.Extra (switchHoldDyn) import ENCOINS.App.Widgets.Coin (coinV3) import ENCOINS.Bulletproofs (Secret) -import ENCOINS.Common.Cache (encoinsV1, encoinsV2, encoinsV3) -import ENCOINS.Common.Events - +import ENCOINS.Common.Cache (encoinsV1, encoinsV2, encoinsV3, loadAppData, saveAppData) +import ENCOINS.Common.Widgets.Advanced (tellAppStatus) {- Evolutions of encoins cache by key 1. encoins - first version of cache that contains Secrets only @@ -79,8 +74,9 @@ migrateCacheV3 mPass ev = do -- As V1 and V2 fire two events on load result, fire saving on second one. eCacheV3 <- tailE $ updated $ zipDynWith migrateV3 dSecretsV1 dSecretsV2 eSaved <- saveAppData mPass encoinsV3 eCacheV3 - eTokensV3 :: Event t [TokenCacheV3] <- updated <$> - loadAppData mPass encoinsV3 "migrateCacheV3-key-eSecretsV3" eSaved id [] + eTokensV3 :: Event t [TokenCacheV3] <- + updated + <$> loadAppData mPass encoinsV3 "migrateCacheV3-key-eSecretsV3" eSaved id [] -- migration is too quick, that's why we delay Success message eMigSuccess <- delay 2 eTokensV3 diff --git a/frontend/src/ENCOINS/App/Widgets/Navbar.hs b/frontend/src/ENCOINS/App/Widgets/Navbar.hs index fdaa3d9e..a9821c96 100755 --- a/frontend/src/ENCOINS/App/Widgets/Navbar.hs +++ b/frontend/src/ENCOINS/App/Widgets/Navbar.hs @@ -8,12 +8,12 @@ import Reflex.Dom import Backend.Protocol.Types (PasswordRaw) import Backend.Status (CloudIconStatus (..)) -import Backend.Utility (space) +import Common.Utility (space) import Backend.Wallet (Wallet (..), currentNetworkApp) -import ENCOINS.Common.Events +import Common.Events import ENCOINS.Common.Widgets.Basic (logo) import ENCOINS.Common.Widgets.Connect (connectWidget) -import ENCOINS.Common.Widgets.MoreMenu (NavMoreMenuClass (..), moreMenuWidget) +import ENCOINS.Common.Widgets.MoreMenu (NavMoreMenuClass (..), viewMoreMenu) navbarWidget :: (MonadWidget t m) => @@ -50,7 +50,7 @@ navbarWidget w dIsBlockAll mPass dIsCloudOn dCloudStatus dIsBlockConnect = do eCloud <- cloudIconWidget dIsCloudOn dIsBlockAll dCloudStatus eLocker <- lockerWidget mPass dIsBlockAll eMore <- - moreMenuWidget + viewMoreMenu (NavMoreMenuClass "common-Nav_Container_MoreMenu" "common-Nav_MoreMenu") pure (eLocker, eConnect, eCloud, eMore) diff --git a/frontend/src/ENCOINS/App/Widgets/Notification.hs b/frontend/src/ENCOINS/App/Widgets/Notification.hs index e3351f62..b6334013 100644 --- a/frontend/src/ENCOINS/App/Widgets/Notification.hs +++ b/frontend/src/ENCOINS/App/Widgets/Notification.hs @@ -11,19 +11,19 @@ import Backend.Status ( AppStatus (..) , CloudIconStatus (..) , WalletStatus (..) - , isAppTotalBlock , isAppStatusWantReload + , isAppTotalBlock + , isAppTxProcessingBlock , isCloudIconStatus - , textAppStatus , isTextAppStatus - , isAppTxProcessingBlock + , textAppStatus ) -import Backend.Utility (space, switchHoldDyn, toText) import Backend.Wallet (Wallet (..)) +import Common.Events +import Common.Reflex.Dom.Extra (elementResultJS) +import Common.Reflex.Extra (switchHoldDyn) +import Common.Utility (singletonL, space, toText) import Config.Config (NetworkConfig (..), networkConfig) -import ENCOINS.App.Widgets.Basic (elementResultJS, singletonL) - -import ENCOINS.Common.Events fetchWalletNetworkStatus :: (MonadWidget t m) => @@ -80,7 +80,12 @@ handleAppStatus dWallet eAppStatusList eOtherTxStatus = do dCloudIconStatus <- holdDyn NoTokens eCloudIconStatus - pure (snd <$> dStatusText, dIsBlockAllButtons, dCloudIconStatus, dIsBlockConnectButton) + pure + ( snd <$> dStatusText + , dIsBlockAllButtons + , dCloudIconStatus + , dIsBlockConnectButton + ) handleNotification :: [AppStatus] -> (AppStatus, Text) -> Maybe (AppStatus, Text) diff --git a/frontend/src/ENCOINS/App/Widgets/PasswordWindow.hs b/frontend/src/ENCOINS/App/Widgets/PasswordWindow.hs index 33d13e55..00f6aea7 100644 --- a/frontend/src/ENCOINS/App/Widgets/PasswordWindow.hs +++ b/frontend/src/ENCOINS/App/Widgets/PasswordWindow.hs @@ -12,13 +12,13 @@ import Witherable (catMaybes) import Backend.Protocol.StrongTypes (PasswordHash (getPassHash), toPasswordHash) import Backend.Protocol.Types (PasswordRaw (..)) -import Backend.Utility (hashKeccak512, isHashOfRaw, switchHoldDyn) -import ENCOINS.App.Widgets.Basic (saveAppData_) -import ENCOINS.Common.Cache (encoinsV3, passwordStorageKey) -import ENCOINS.Common.Events -import ENCOINS.Common.Events (setFocusDelayOnEvent) +import Common.Events +import Common.Events (setFocusDelayOnEvent) +import Common.Reflex.Extra (switchHoldDyn) +import Common.Utility (hashKeccak512, isHashOfRaw) +import ENCOINS.Common.Cache (encoinsV3, passwordStorageKey, saveAppData_) import ENCOINS.Common.Widgets.Advanced (dialogWindow) -import ENCOINS.Common.Widgets.Basic (br, btn, errDiv) +import ENCOINS.Common.Widgets.Basic (br, btn, divClassDyn) import JS.App (loadCacheValue, saveHashedTextToStorage) validatePassword :: Text -> Either Text PasswordRaw @@ -53,58 +53,53 @@ enterPasswordWindow :: -> m (Event t PasswordRaw, Event t ()) enterPasswordWindow passHash eResetOk = mdo dWindowIsOpen <- holdDyn True (False <$ leftmost [void eClose, eResetOk]) - let windowStyle = - "width: min(90%, 750px); padding-left: min(5%, 70px); padding-right: min(5%, 70px); padding-top: min(5%, 30px); padding-bottom: min(5%, 30px)" - ret@(eClose, _) <- elDynAttr "div" (fmap mkClass dWindowIsOpen) - $ elAttr - "div" - ("class" =: "dialog-window" <> "style" =: windowStyle) - $ do - divClass "app-columns w-row" $ - divClass "connect-title-div" $ - divClass "app-text-semibold" $ - text "Password for the cache of Encoins app" - dPassOk <- divClass "app-columns w-row" $ - divClass "w-col w-col-12" $ do - ePb <- getPostBuild - dmCurPass <- passwordInput "Enter password:" False True (pure Nothing) ePb - pure $ checkPass passHash <$> dmCurPass - (eClean, eOk) <- elAttr "div" ("class" =: "app-columns w-row app-EnterPassword_ButtonContainer") $ do - eSave' <- - btn - "button-switching inverted flex-center" - "" - $ text "Ok" - eClean' <- - btn - "button-switching flex-center" - "" - $ text "Clean cache" - return (eClean', eSave') - widgetHold_ blank $ - leftmost - [ maybe err (const blank) <$> tagPromptlyDyn dPassOk eOk - , blank <$ updated dPassOk - ] - return (catMaybes $ tagPromptlyDyn dPassOk eOk, eClean) - return ret + ret@(eClose, _) <- do + divClassDyn (mkClass <$> dWindowIsOpen) $ mdo + (eClean, eOk, dPass) <- viewEnterPasswordEntries eError + let dPassOk = checkPass passHash <$> dPass + let eError = + leftmost + [ maybe (viewPasswordError "Incorrect password") (const blank) + <$> tagPromptlyDyn dPassOk eOk + , blank <$ updated dPassOk + ] + pure (catMaybes $ tagPromptlyDyn dPassOk eOk, eClean) + pure ret where - err = - elAttr - "div" - ( "class" =: "app-columns w-row" - <> "style" =: "display:flex;justify-content:center;" - ) - $ errDiv "Incorrect password" - mkClass b = - "class" =: "dialog-window-wrapper" - <> bool ("style" =: "display: none") mempty b + mkClass b = bool "app-EnterPasswordWindow-none" "app-EnterPasswordWindow" b checkPass hash mRaw = do raw <- mRaw if isHashOfRaw (getPassHash hash) (getPassRaw raw) then Just raw else Nothing +viewEnterPasswordEntries :: + (MonadWidget t m) => + Event t (m ()) -- password error + -> m (Event t (), Event t (), Dynamic t (Maybe PasswordRaw)) +viewEnterPasswordEntries eError = divClass "app-DialogWindow_EnterPassword" $ mdo + divClass "w-row" $ + divClass "connect-title-div" $ + divClass "app-text-semibold" $ + text "Password for the cache of Encoins app" + dPass' <- divClass "w-row" $ + divClass "w-col w-col-12" $ do + ePb <- getPostBuild + passwordInput "Enter password:" False True (pure Nothing) eError ePb + (eClean, eSave) <- divClass "w-row app-EnterPassword_ButtonContainer" $ do + eSave' <- + btn + "button-switching inverted flex-center" + "" + $ text "Ok" + eClean' <- + btn + "button-switching flex-center" + "" + $ text "Clean cache" + pure (eClean', eSave') + pure (eClean, eSave, dPass') + passwordSettingsWindow :: (MonadWidget t m) => Event t () @@ -151,7 +146,7 @@ passwordButtons dmPassHash dPassOk dmNewPass = do mkSaveBtnCls _ _ _ = cls <> " button-disabled" mkClearBtnCls = (cls <>) . bool " button-disabled" "" divClass - "app-columns w-row app-PasswordSetting_ButtonContainer" + "w-row app-PasswordSetting_ButtonContainer" $ do eSave' <- do let dSaveClass = mkSaveBtnCls <$> dmPassHash <*> dPassOk <*> dmNewPass @@ -188,14 +183,15 @@ passwordChecker :: passwordChecker dmPassHash eOpen = do let mkErr _ Nothing = blank mkErr _ (Just (PasswordRaw "")) = blank - mkErr c _ = bool (errDiv "Incorrect password") blank c + mkErr c _ = bool (viewPasswordError "Incorrect password") blank c checkPass hash (Just raw) = isHashOfRaw (getPassHash hash) (getPassRaw raw) checkPass _ Nothing = False ePassOk <- switchHoldDyn dmPassHash $ \case - Just passHash -> divClass "app-columns w-row" $ divClass "w-col w-col-12" $ do - dmCurPass <- passwordInput "Current password:" False True (pure Nothing) eOpen + Just passHash -> divClass "w-row" $ divClass "w-col w-col-12" $ mdo + dmCurPass <- + passwordInput "Current password:" False True (pure Nothing) eError eOpen let dCheckedPass = checkPass passHash <$> dmCurPass - dyn_ $ mkErr <$> dCheckedPass <*> dmCurPass + let eError = updated $ mkErr <$> dCheckedPass <*> dmCurPass return (ffilter id $ updated dCheckedPass) Nothing -> pure never holdDyn False ePassOk @@ -207,9 +203,9 @@ passwordEnterRepeat :: passwordEnterRepeat eOpen = divClass "app-PasswordProtect_Window" $ do dmPass1 <- divClass "w-col" $ do - passwordInput "Enter password:" False True (pure Nothing) eOpen + passwordInput "Enter password:" False True (pure Nothing) never eOpen dmPass2 <- divClass "w-col" $ do - passwordInput "Repeat password:" True False dmPass1 eOpen + passwordInput "Repeat password:" True False dmPass1 never eOpen return dmPass2 passwordInput :: @@ -218,16 +214,19 @@ passwordInput :: -> Bool -> Bool -> Dynamic t (Maybe PasswordRaw) + -> Event t (m ()) -- Incorrect password error -> Event t () -> m (Dynamic t (Maybe PasswordRaw)) -passwordInput txt rep isFocus dmPass eOpen = mdo +passwordInput txt rep isFocus dmPass eError eOpen = mdo dShowPass <- toggle False (domEvent Click eye) - appTextLeft txt + divClass "app-PasswordError_Container" $ do + appTextLeft txt + dyn_ $ mkError <$> value inp <*> deVal <*> dmPass -- view invalid password input error + widgetHold_ blank eError -- view incorrect password error inp <- inputElement $ conf $ bool "password" "text" <$> updated dShowPass if isFocus then setFocusDelayOnEvent inp eOpen else blank (eye, _) <- elDynAttr' "i" (mkEyeAttr <$> dShowPass) blank let deVal = validatePassword <$> value inp - dyn_ $ mkError <$> value inp <*> deVal <*> dmPass return (zipDynWith mkRes deVal dmPass) where mkRes (Right p1) (Just p2) = @@ -244,24 +243,18 @@ passwordInput txt rep isFocus dmPass eOpen = mdo then if p1 == p2 then blank - else errDiv "Password doesn't match" + else viewPasswordError "Password doesn't match" else blank mkError _ (Right _) Nothing = if rep - then errDiv "Password doesn't match" + then viewPasswordError "Password doesn't match" else blank mkError _ (Left err) _ = if rep - then errDiv "Password doesn't match" - else errDiv err + then viewPasswordError "Password doesn't match" + else viewPasswordError err mkEyeAttr showPass = "class" =: ("app-Eye_Input far " <> bool "fa-eye" "fa-eye-slash" showPass) - appTextLeft = - elAttr - "div" - ( "class" =: "app-text-normal" - <> "style" =: "justify-content: left;" - ) - . text + appTextLeft = divClass "app-Password_InputTitle" . text conf eType = def & initialAttributes @@ -289,7 +282,7 @@ cleanCacheDialog eOpen = mdo text "This action will reset password and clean cache (remove known coins)!" br text "Are you sure?" - elAttr "div" ("class" =: "app-columns w-row app-CleanCache_ButtonContainer") $ do + divClass "w-row app-CleanCache_ButtonContainer" $ do btnOk <- btn "button-switching inverted flex-center" "" $ text "Clean" btnCancel <- btn "button-switching flex-center" "" $ text "Cancel" return (btnOk, btnCancel) @@ -297,3 +290,6 @@ cleanCacheDialog eOpen = mdo (saveHashedTextToStorage passwordStorageKey (hashKeccak512 "") <$ eOk) saveAppData_ Nothing encoinsV3 $ ("" :: Text) <$ eOk return eOk + +viewPasswordError :: (MonadWidget t m) => Text -> m () +viewPasswordError = divClass "app-PasswordError_Message" . text diff --git a/frontend/src/ENCOINS/App/Widgets/ReEncryption.hs b/frontend/src/ENCOINS/App/Widgets/ReEncryption.hs index d0e03a38..1de03530 100644 --- a/frontend/src/ENCOINS/App/Widgets/ReEncryption.hs +++ b/frontend/src/ENCOINS/App/Widgets/ReEncryption.hs @@ -6,11 +6,11 @@ import Data.Text (Text) import Reflex.Dom import Backend.Protocol.Types (AesKeyRaw, PasswordRaw (..), TokenCacheV3) -import ENCOINS.App.Widgets.Basic (loadAppDataM) +import Common.Events +import Common.Utility (toJsonText) +import ENCOINS.Common.Cache (loadAppDataM) import ENCOINS.Bulletproofs (Secret) import ENCOINS.Common.Cache (aesKey, encoinsV1, encoinsV2, encoinsV3) -import ENCOINS.Common.Events -import ENCOINS.Common.Utils (toJsonText) import JS.Website (saveJSON) ------------------------------------------------------------------------------- diff --git a/frontend/src/ENCOINS/App/Widgets/SendRequestButton.hs b/frontend/src/ENCOINS/App/Widgets/SendRequestButton.hs index 01a393ea..952f5aa3 100755 --- a/frontend/src/ENCOINS/App/Widgets/SendRequestButton.hs +++ b/frontend/src/ENCOINS/App/Widgets/SendRequestButton.hs @@ -13,14 +13,15 @@ import Backend.Protocol.TxValidity ) import Backend.Protocol.Types import Backend.Servant.Requests (getRelayUrlE, statusRequestWrapper) -import Backend.Status (AppStatus, WalletTxStatus (..), LedgerTxStatus (..)) -import Backend.Utility (switchHoldDyn, toEither) +import Backend.Status (AppStatus, LedgerTxStatus (..), WalletTxStatus (..)) import Backend.Wallet (Wallet (..)) +import Common.Reflex.Extra (switchHoldDyn) +import Common.Utility (toEither) import ENCOINS.Bulletproofs (Secrets) -import ENCOINS.Common.Widgets.Advanced (updateUrls) +import Common.Reflex.Extra (updateUrls) import ENCOINS.Common.Widgets.Basic (btn, divClassId) -import ENCOINS.Common.Events +import Common.Events sendRequestButtonWallet :: (MonadWidget t m) => @@ -77,14 +78,7 @@ sendRequestButtonWallet _ -> "button-not-selected button-disabled flex-center" g v = case v of TxValid -> blank - TxInvalid err -> - elAttr - "div" - ( "class" =: "div-tooltip div-tooltip-always-visible" - <> "style" =: "border-top-left-radius: 0px; border-top-right-radius: 0px" - ) - $ divClass "app-text-normal" - $ text err + TxInvalid err -> viewTxInvalidTooltip err h v = case v of TxValid -> "" _ -> "border-bottom-left-radius: 0px; border-bottom-right-radius: 0px" @@ -142,14 +136,7 @@ sendRequestButtonLedger mode dStatus dCoinsToBurn dCoinsToMint e dUrls = mdo _ -> "button-not-selected button-disabled flex-center" g v = case v of TxValid -> blank - TxInvalid err -> - elAttr - "div" - ( "class" =: "div-tooltip div-tooltip-always-visible" - <> "style" =: "border-top-left-radius: 0px; border-top-right-radius: 0px" - ) - $ divClass "app-text-normal" - $ text err + TxInvalid err -> viewTxInvalidTooltip err h v = case v of TxValid -> "" _ -> "border-bottom-left-radius: 0px; border-bottom-right-radius: 0px" @@ -160,3 +147,7 @@ sendRequestButtonLedger mode dStatus dCoinsToBurn dCoinsToMint e dUrls = mdo dyn_ $ fmap g dTxValidity let eValidTx = () <$ ffilter (== TxValid) (current dTxValidity `tag` eSend) pure (LedTxNoRelay <$ eAllRelayDown, eValidTx) + +viewTxInvalidTooltip :: (MonadWidget t m) => Text -> m () +viewTxInvalidTooltip = + divClass "app-SendButton_Tooltip_TxInvalid" . divClass "app-text-normal" . text diff --git a/frontend/src/ENCOINS/App/Widgets/SendToWalletWindow.hs b/frontend/src/ENCOINS/App/Widgets/SendToWalletWindow.hs index 59c40a1f..867e231e 100755 --- a/frontend/src/ENCOINS/App/Widgets/SendToWalletWindow.hs +++ b/frontend/src/ENCOINS/App/Widgets/SendToWalletWindow.hs @@ -4,7 +4,7 @@ module ENCOINS.App.Widgets.SendToWalletWindow where import Reflex.Dom -import Backend.Protocol.Utility (secretToHex) +import Common.Protocol (secretToHex) import ENCOINS.Bulletproofs (Secrets) import ENCOINS.Common.Widgets.Advanced (dialogWindow) import ENCOINS.Common.Widgets.Basic (br, btn) @@ -16,23 +16,19 @@ sendToWalletWindow eOpen dSecrets = mdo divClass "connect-title-div" $ divClass "app-text-semibold" $ text "Copy and send these keys to your recepient off-chain:" - elAttr - "div" - ( "class" =: "app-text-normal" - <> "style" =: "justify-content: space-between;text-align:left;" - ) $ + divClass "app-Transfer_SendToWalletWindow_Secret" $ dyn_ $ mapM ((>> br) . text . secretToHex) <$> dSecrets br btnOk <- btn "button-switching inverted flex-center" - "width:30%;display:inline-block;margin-right:5px;" $ - text "Ok" + "width:30%;display:inline-block;margin-right:5px;" + $ text "Ok" btnCancel <- btn "button-switching flex-center" - "width:30%;display:inline-block;margin-left:5px;" $ - text "Cancel" + "width:30%;display:inline-block;margin-left:5px;" + $ text "Cancel" return (btnOk, btnCancel) return eOk diff --git a/frontend/src/ENCOINS/App/Widgets/TabsSelection.hs b/frontend/src/ENCOINS/App/Widgets/TabsSelection.hs index 214e2cf2..7dfa6e3d 100755 --- a/frontend/src/ENCOINS/App/Widgets/TabsSelection.hs +++ b/frontend/src/ENCOINS/App/Widgets/TabsSelection.hs @@ -3,8 +3,7 @@ module ENCOINS.App.Widgets.TabsSelection where import Data.Bool (bool) import Reflex.Dom -import ENCOINS.App.Widgets.Basic (containerApp, sectionApp) -import ENCOINS.Common.Widgets.Basic (btnWithBlock, divClassId) +import ENCOINS.Common.Widgets.Basic (btnWithBlock, containerApp, divClassId, sectionApp) data AppTab = WalletTab diff --git a/frontend/src/ENCOINS/App/Widgets/TransactionBalance.hs b/frontend/src/ENCOINS/App/Widgets/TransactionBalance.hs index bc6f8b46..8ea0b7ca 100755 --- a/frontend/src/ENCOINS/App/Widgets/TransactionBalance.hs +++ b/frontend/src/ENCOINS/App/Widgets/TransactionBalance.hs @@ -7,7 +7,7 @@ import Data.Text (Text) import Reflex.Dom import Backend.Protocol.Types (EncoinsMode (..)) -import Backend.Utility (column, space, toText) +import Common.Utility (column, space, toText) import ENCOINS.Common.Widgets.Basic (br, divClassId, image) @@ -56,11 +56,7 @@ transactionBalanceWidget formula mMode txt = do dyn_ $ bool blank (formulaTooltip formula mode) <$> dIsTooltipVisible formulaTooltip :: (MonadWidget t m) => Formula t -> EncoinsMode -> m () -formulaTooltip Formula{..} mode = elAttr - "div" - ( "class" =: "app-Formula_TooltipWrapper" - <> "style" =: "border-top-left-radius: 0px; border-top-right-radius: 0px" - ) +formulaTooltip Formula{..} mode = divClass "app-Formula_TooltipWrapper" $ do divClass "app-text-semibold" $ text "Balance formula" elAttr diff --git a/frontend/src/ENCOINS/App/Widgets/WelcomeWindow.hs b/frontend/src/ENCOINS/App/Widgets/WelcomeWindow.hs index 1f70a97d..184516e4 100755 --- a/frontend/src/ENCOINS/App/Widgets/WelcomeWindow.hs +++ b/frontend/src/ENCOINS/App/Widgets/WelcomeWindow.hs @@ -11,8 +11,8 @@ import qualified Data.Text.Encoding as TE import Reflex.Dom import Text.RawString.QQ (r) -import Backend.Utility (toText) -import ENCOINS.App.Widgets.Basic (loadJsonFromStorage, saveJsonToStorage) +import Common.Utility (toText) +import ENCOINS.Common.Cache (loadJsonFromStorage, saveJsonToStorage) import ENCOINS.Common.Widgets.Advanced (dialogWindow) import ENCOINS.Common.Widgets.Basic (btn, lnkInlineInverted) import JS.Website (setElementStyle) diff --git a/frontend/src/ENCOINS/Common/Cache.hs b/frontend/src/ENCOINS/Common/Cache.hs index e1ff8a09..e46ea327 100644 --- a/frontend/src/ENCOINS/Common/Cache.hs +++ b/frontend/src/ENCOINS/Common/Cache.hs @@ -1,6 +1,26 @@ module ENCOINS.Common.Cache where +import Backend.Protocol.Types (PasswordRaw (..)) +import Common.Events +import Common.Utility (toJsonText) +import JS.Website (loadJSON, removeKey, saveJSON) + +import Common.Reflex.Dom.Extra (elementResultJS) +import Control.Monad (void) +import Data.Aeson (FromJSON, ToJSON, decode, decodeStrict) +import Data.ByteString (ByteString) +import Data.ByteString.Lazy (fromStrict) import Data.Text (Text) +import Data.Text.Encoding (encodeUtf8) +import GHCJS.DOM (currentWindowUnchecked) +import GHCJS.DOM.Storage (getItem, setItem) +import GHCJS.DOM.Types (MonadDOM) +import GHCJS.DOM.Window (getLocalStorage) +import Reflex.Dom + +------------------------------------------------------------------------------- +-- Constants for browser cache +------------------------------------------------------------------------------- encoinsV3 :: Text encoinsV3 = "encoins-v3" @@ -26,3 +46,102 @@ isCloudOn = "encoins-save-on" passwordStorageKey :: Text passwordStorageKey = "password-hash" + +------------------------------------------------------------------------------- +-- Cache functions +------------------------------------------------------------------------------- + +loadAppDataE :: + forall t m a b. + (MonadWidget t m, FromJSON a, Show a, Show b) => + Maybe PasswordRaw + -> Text -- cache key + -> Text -- response id + -> (a -> b) + -> b + -> m (Dynamic t b) +loadAppDataE mPass key resId f val = do + e <- newEventWithDelay 0.1 + loadAppData mPass key resId e f val + +loadAppData :: + forall t m a b. + (MonadWidget t m, FromJSON a, Show a, Show b) => + Maybe PasswordRaw + -> Text -- cache key + -> Text -- response id + -> Event t () + -> (a -> b) + -> b + -> m (Dynamic t b) +loadAppData mPass key resId ev f val = do + dmRes <- loadAppDataM mPass key resId ev + let dRes = maybe val f <$> dmRes + pure dRes + +loadAppDataME :: + forall t m a. + (MonadWidget t m, FromJSON a, Show a) => + Maybe PasswordRaw + -> Text -- cache key + -> Text -- response id + -> m (Dynamic t (Maybe a)) +loadAppDataME mPass key resId = do + e <- newEventWithDelay 0.1 + loadAppDataM mPass key resId e + +loadAppDataM :: + forall t m a. + (MonadWidget t m, FromJSON a, Show a) => + Maybe PasswordRaw + -> Text -- cache key + -> Text -- response id + -> Event t () + -> m (Dynamic t (Maybe a)) +loadAppDataM mPass key resId ev = do + let mPassT = (getPassRaw <$> mPass) + performEvent_ (loadJSON key resId mPassT <$ ev) + dRes <- + elementResultJS resId ((decodeStrict :: ByteString -> Maybe a) . encodeUtf8) + pure dRes + +saveAppData_ :: + (MonadWidget t m, ToJSON a) => + Maybe PasswordRaw + -> Text + -> Event t a + -> m () +saveAppData_ mPass key eVal = do + void $ saveAppData mPass key eVal + +saveAppData :: + (MonadWidget t m, ToJSON a) => + Maybe PasswordRaw + -> Text + -> Event t a + -> m (Event t ()) +saveAppData mPass key eVal = do + let eEncodedValue = toJsonText <$> eVal + let mPassT = (getPassRaw <$> mPass) + performEvent (saveJSON mPassT key <$> eEncodedValue) + +removeCacheKey :: + (MonadWidget t m) => + Event t Text + -> m (Event t ()) +removeCacheKey eKey = performEvent (removeKey <$> eKey) + +loadJsonFromStorage :: (MonadDOM m, FromJSON a) => Text -> m (Maybe a) +loadJsonFromStorage elId = do + lc <- currentWindowUnchecked >>= getLocalStorage + (>>= decode . fromStrict . encodeUtf8) <$> getItem lc elId + +saveJsonToStorage :: (MonadDOM m, ToJSON a) => Text -> a -> m () +saveJsonToStorage elId val = do + lc <- currentWindowUnchecked >>= getLocalStorage + setItem lc elId . toJsonText $ val + +loadTextFromStorage :: (MonadDOM m) => Text -> m (Maybe Text) +loadTextFromStorage key = do + lc <- currentWindowUnchecked >>= getLocalStorage + getItem lc key diff --git a/frontend/src/ENCOINS/Common/ConnectWindow.hs b/frontend/src/ENCOINS/Common/ConnectWindow.hs new file mode 100755 index 00000000..055b77eb --- /dev/null +++ b/frontend/src/ENCOINS/Common/ConnectWindow.hs @@ -0,0 +1,60 @@ +{-# LANGUAGE RecursiveDo #-} + +module ENCOINS.Common.ConnectWindow + ( connectWindow + ) where + +import Control.Monad (void) +import Data.Bool (bool) +import Reflex.Dom + +import Backend.Wallet (Wallet (..), WalletName (..), fromJS, toJS) +import Common.Utility (toText) +import ENCOINS.Common.Cache (currentWallet, loadAppDataE, saveAppData_) +import ENCOINS.Common.Widgets.Advanced (dialogWindow) +import ENCOINS.Common.Widgets.Wallet (loadWallet, walletIcon) + +viewWalletEntry :: (MonadWidget t m) => WalletName -> m (Event t WalletName) +viewWalletEntry w = do + (e, _) <- elAttr' "div" ("class" =: "connect-wallet-div") $ do + divClass "app-text-normal" $ text $ bool "Disconnect" (toText w) $ w /= None + elAttr + "a" + ( "href" =: "#" + <> "class" =: "w-inline-block" + <> "style" =: "margin-left:150px;" + ) + $ bool blank (walletIcon w) + $ w /= None + return (w <$ domEvent Click e) + +connectWindow :: + (MonadWidget t m) => [WalletName] -> Event t () -> m (Dynamic t Wallet) +connectWindow supportedWallets eConnectOpen = mdo + (eConnectClose, dWallet) <- dialogWindow + True + eConnectOpen + eConnectClose + "common-ConnectWindow" + "Connect Wallet" + $ mdo + eWalletName <- + divClass "common-Connect_WalletContainer" $ + leftmost . ([eLastWalletName] ++) <$> mapM viewWalletEntry supportedWallets + eUpdate <- tag bWalletName <$> tickLossyFromPostBuildTime 10 + dW <- loadWallet (leftmost [eWalletName, eUpdate]) >>= holdUniqDyn + let bWalletName = current $ fmap walletName dW + + -- save/load wallet + saveAppData_ Nothing currentWallet $ toJS <$> eWalletName + eLastWalletName <- + updated + <$> loadAppDataE + Nothing + currentWallet + "connectWindow-key-currentWallet" + fromJS + None + + return (void eWalletName, dW) + return dWallet diff --git a/frontend/src/ENCOINS/Common/Widgets/Advanced.hs b/frontend/src/ENCOINS/Common/Widgets/Advanced.hs index 30a84e13..31e8cd0e 100755 --- a/frontend/src/ENCOINS/Common/Widgets/Advanced.hs +++ b/frontend/src/ENCOINS/Common/Widgets/Advanced.hs @@ -3,8 +3,6 @@ module ENCOINS.Common.Widgets.Advanced where import Data.Bool (bool) -import Data.List ((\\)) -import Data.Maybe (isJust) import Data.Text (Text) import Data.Time (NominalDiffTime) import GHCJS.DOM (currentDocumentUnchecked) @@ -14,9 +12,16 @@ import GHCJS.DOM.Node (contains) import qualified GHCJS.DOM.Types as DOM import Reflex.Dom -import Backend.Utility (space, toText) +import Backend.Status + ( AppStatus (..) + , WalletStatus (..) + ) +import Common.Events +import Common.Reflex.Dom.Extra (elementResultJS) +import Common.Utility (singletonL, space, toText) import Config.Config (NetworkId) import JS.Website (setElementStyle) +import Reflex.ScriptDependent (widgetHoldUntilDefined) copyEvent :: (MonadWidget t m) => Event t () -> m (Dynamic t Bool) copyEvent e = do @@ -27,15 +32,15 @@ copyEvent e = do (setElementStyle "bottom-notification-copy" "display" "none" <$ e') return d -copyButton :: (MonadWidget t m) => m (Event t ()) -copyButton = mdo +viewCopyButton :: (MonadWidget t m) => m (Event t ()) +viewCopyButton = mdo let mkClass = bool "copy-div" "tick-div inverted" e <- domEvent Click . fst <$> elDynClass' "div" (fmap mkClass d) blank d <- copyEvent e return e -copiedNotification :: (MonadWidget t m) => m () -copiedNotification = +viewCopiedNotification :: (MonadWidget t m) => m () +viewCopiedNotification = elAttr "div" ( "class" =: "bottom-notification" @@ -45,8 +50,8 @@ copiedNotification = . divClass "notification-content" $ text "Copied!" -noRelayNotification :: (MonadWidget t m) => m () -noRelayNotification = +viewNoRelayNotification :: (MonadWidget t m) => m () +viewNoRelayNotification = elAttr "div" ( "class" =: "bottom-notification" @@ -57,8 +62,8 @@ noRelayNotification = $ text "All available relays are down! Try reloading the page or come back later." -wrongNetworkNotification :: (MonadWidget t m) => NetworkId -> m () -wrongNetworkNotification network = +viewWrongNetworkNotification :: (MonadWidget t m) => NetworkId -> m () +viewWrongNetworkNotification network = elAttr "div" ( "class" =: "bottom-notification" @@ -71,8 +76,8 @@ wrongNetworkNotification network = <> toText network <> "." -checkboxButton :: (MonadWidget t m) => m (Dynamic t Bool) -checkboxButton = mdo +viewCheckboxButton :: (MonadWidget t m) => m (Dynamic t Bool) +viewCheckboxButton = mdo let mkClass = bool "checkbox-div" "checkbox-div checkbox-selected" (e, _) <- elDynClass' "div" (fmap mkClass d) blank d <- toggle False $ domEvent Click e @@ -160,33 +165,26 @@ withTooltip mainW tipClass delay1 delay2 innerW = mdo showAttrs = constAttrs <> "style" =: "display:inline-block;" hideAttrs = constAttrs <> "style" =: "display:none;" -foldDynamicAny :: (Reflex t) => [Dynamic t Bool] -> Dynamic t Bool -foldDynamicAny = foldr (zipDynWith (||)) (constDyn False) - -updateUrls :: - (MonadWidget t m) => - Dynamic t [Text] - -> Event t (Maybe Text) - -> m (Dynamic t [Text]) -updateUrls dUrls eFailedUrl = do - dFailedUrls <- - foldDyn (\mUrl acc -> maybe acc (\u -> u : acc) mUrl) [] eFailedUrl - pure $ zipDynWith (\\) dUrls dFailedUrls - --- Fire 'Main event' only when there is some value in Condition event. -fireWhenJustThenReset :: - (MonadWidget t m) => - Event t a -- Main event - -> Event t (Maybe b) -- Condition event - -> Event t c -- Reset event - -> m (Event t ()) -fireWhenJustThenReset eMain eCondition eReset = do - -- Hold 'Main event' as True value , - -- and then after 'Reset event' fires - -- reset it to False. - dIsMain <- holdDyn False $ leftmost [True <$ eMain, False <$ eReset] - pure $ - attachPromptlyDynWithMaybe - (\isMain mCondition -> if isMain && isJust mCondition then Just () else Nothing) - dIsMain - eCondition +waitForScripts :: (MonadWidget t m) => m () -> m () -> m () +waitForScripts placeholderWidget actualWidget = do + ePB <- getPostBuild + _ <- + widgetHoldUntilDefined + "walletAPI" + ("js/ENCOINS.js" <$ ePB) + placeholderWidget + actualWidget + blank + +-- Wallet error element +walletError :: (MonadWidget t m) => m (Event t WalletStatus) +walletError = do + dWalletError <- elementResultJS "walletErrorElement" id + let eWalletError = ffilter ("" /=) $ updated dWalletError + return $ WalletFail <$> eWalletError + +tellAppStatus :: + (MonadWidget t m, EventWriter t [AppStatus] m) => + Event t AppStatus + -> m () +tellAppStatus ev = tellEvent $ singletonL <$> ev diff --git a/frontend/src/ENCOINS/Common/Widgets/Basic.hs b/frontend/src/ENCOINS/Common/Widgets/Basic.hs old mode 100755 new mode 100644 index 45ab5b2d..7048f8ec --- a/frontend/src/ENCOINS/Common/Widgets/Basic.hs +++ b/frontend/src/ENCOINS/Common/Widgets/Basic.hs @@ -6,7 +6,7 @@ import Data.Text (Text, unpack) import qualified Data.Text as T import Reflex.Dom -import Backend.Utility (space) +import Common.Utility (space) h1 :: (MonadWidget t m) => Text -> m () h1 = elClass "h1" "h1" . text @@ -173,3 +173,15 @@ notification :: (MonadWidget t m) => Dynamic t Text -> m () notification dNotification = do divClass "notification" $ do divClass "notification-text" $ dynText dNotification + +divClassDyn :: (MonadWidget t m) => Dynamic t Text -> m a -> m a +divClassDyn = elDynClass "div" + +sectionApp :: (MonadWidget t m) => Text -> Text -> m a -> m a +sectionApp elemId cls = + elAttr + "div" + ("id" =: elemId <> "class" =: "section-app wf-section " `T.append` cls) + +containerApp :: (MonadWidget t m) => Text -> m a -> m a +containerApp cls = divClass ("container-app w-container " `T.append` cls) \ No newline at end of file diff --git a/frontend/src/ENCOINS/Common/Widgets/Connect.hs b/frontend/src/ENCOINS/Common/Widgets/Connect.hs index 0412788b..8df0c196 100644 --- a/frontend/src/ENCOINS/Common/Widgets/Connect.hs +++ b/frontend/src/ENCOINS/Common/Widgets/Connect.hs @@ -20,10 +20,11 @@ connectWidget :: Dynamic t Wallet -> Dynamic t Bool -> m (Event t ()) -connectWidget dWallet dIsBlockedConnect = divClass "menu-item-button-left" $ - btnWithBlock +connectWidget dWallet dIsBlockedConnect = divClass "menu-item-button-left" + $ btnWithBlock "button-switching flex-center common-Connect_Button" "" - dIsBlockedConnect $ do + dIsBlockedConnect + $ do dyn_ $ fmap (walletIcon . walletName) dWallet dynText $ fmap connectText dWallet diff --git a/frontend/src/ENCOINS/Common/Widgets/MoreMenu.hs b/frontend/src/ENCOINS/Common/Widgets/MoreMenu.hs index b84ea1d1..2893ea98 100644 --- a/frontend/src/ENCOINS/Common/Widgets/MoreMenu.hs +++ b/frontend/src/ENCOINS/Common/Widgets/MoreMenu.hs @@ -3,8 +3,8 @@ module ENCOINS.Common.Widgets.MoreMenu where -import Backend.Utility (space) -import ENCOINS.Common.Events +import Common.Utility (space) +import Common.Events import ENCOINS.Common.Widgets.Advanced (dialogWindow) import ENCOINS.Common.Widgets.Basic (lnk) @@ -17,11 +17,11 @@ data NavMoreMenuClass = NavMoreMenuClass , nmmcIcon :: Text } -moreMenuWidget :: +viewMoreMenu :: (MonadWidget t m) => NavMoreMenuClass -> m (Event t ()) -moreMenuWidget cls = do +viewMoreMenu cls = do elMore <- divClass ("menu-item" <> space <> nmmcContainer cls) $ fmap fst $ diff --git a/frontend/src/ENCOINS/Common/Widgets/SelectInput.hs b/frontend/src/ENCOINS/Common/Widgets/SelectInput.hs index 80d3a268..03417906 100755 --- a/frontend/src/ENCOINS/Common/Widgets/SelectInput.hs +++ b/frontend/src/ENCOINS/Common/Widgets/SelectInput.hs @@ -6,8 +6,7 @@ import Reflex.Dom import Text.Read (readMaybe) import Witherable (catMaybes) -import Backend.Utility (toText) -import ENCOINS.Common.Utils (safeIndex) +import Common.Utility (safeIndex, toText) -- TODO: complete and move this to ENCOINS.App.Widgets -- Title of the input element along with a hint about the expected input diff --git a/frontend/src/ENCOINS/Common/Widgets/Wallet.hs b/frontend/src/ENCOINS/Common/Widgets/Wallet.hs index 424f2e57..2dcff938 100644 --- a/frontend/src/ENCOINS/Common/Widgets/Wallet.hs +++ b/frontend/src/ENCOINS/Common/Widgets/Wallet.hs @@ -12,8 +12,8 @@ import Reflex.Dom hiding (Input) import Backend.Protocol.Types (checkEmptyText, mkAddressFromPubKeys) import Backend.Wallet import CSL (TransactionUnspentOutputs) +import Common.Reflex.Dom.Extra (elementResultJS) import Config.Config (NetworkId (..), toNetworkId) -import ENCOINS.App.Widgets.Basic (elementResultJS) import ENCOINS.Common.Widgets.Basic (image) loadWallet :: (MonadWidget t m) => Event t WalletName -> m (Dynamic t Wallet) diff --git a/frontend/src/ENCOINS/DAO/Body.hs b/frontend/src/ENCOINS/DAO/Body.hs index dd0c9fa3..48bbb116 100755 --- a/frontend/src/ENCOINS/DAO/Body.hs +++ b/frontend/src/ENCOINS/DAO/Body.hs @@ -13,20 +13,20 @@ import Data.Time (getCurrentTime) import Reflex.Dom import Backend.Wallet (walletsSupportedInDAO) -import ENCOINS.App.Widgets.Basic (waitForScripts) -import ENCOINS.App.Widgets.ConnectWindow (connectWindow) -import ENCOINS.Common.Events +import ENCOINS.Common.Widgets.Advanced (waitForScripts) +import ENCOINS.Common.ConnectWindow (connectWindow) +import Common.Events import ENCOINS.Common.Widgets.Basic (notification) import ENCOINS.Common.Widgets.JQuery (jQueryWidget) import ENCOINS.Common.Widgets.MoreMenu ( WindowMoreMenuClass (..) , moreMenuWindow ) -import ENCOINS.DAO.Polls +import ENCOINS.DAO.Widgets.Poll.Polls import ENCOINS.DAO.Widgets.DelegateWindow (delegateWindow) import ENCOINS.DAO.Widgets.Navbar (Dao (..), navbarWidget) import ENCOINS.DAO.Widgets.PollWidget -import ENCOINS.DAO.Widgets.RelayTable (fetchRelayNames) +import ENCOINS.DAO.Widgets.DelegateWindow.RelayTable (fetchRelayNames) import ENCOINS.DAO.Widgets.StatusWidget import ENCOINS.Website.Widgets.Basic (container, section) diff --git a/frontend/src/ENCOINS/DAO/Widgets/DelegateWindow.hs b/frontend/src/ENCOINS/DAO/Widgets/DelegateWindow.hs index e1e12641..fcaeb6e3 100644 --- a/frontend/src/ENCOINS/DAO/Widgets/DelegateWindow.hs +++ b/frontend/src/ENCOINS/DAO/Widgets/DelegateWindow.hs @@ -13,14 +13,13 @@ import qualified Data.Text as T import Reflex.Dom import Backend.Status (UrlStatus (..), isNotValidUrl) -import Backend.Utility (toText) import Backend.Wallet (LucidConfig (..), Wallet (..), lucidConfigDao, toJS) -import ENCOINS.App.Widgets.Basic (containerApp) -import ENCOINS.Common.Events -import ENCOINS.Common.Utils (checkUrl, stripHostOrRelay) +import Common.Events +import Common.Url (checkUrl, stripHostOrRelay) +import Common.Utility (toText) import ENCOINS.Common.Widgets.Advanced (dialogWindow) -import ENCOINS.Common.Widgets.Basic (btn, btnWithBlock, divClassId) -import ENCOINS.DAO.Widgets.RelayTable +import ENCOINS.Common.Widgets.Basic (btn, btnWithBlock, containerApp, divClassId) +import ENCOINS.DAO.Widgets.DelegateWindow.RelayTable ( fetchDelegatedByAddress , fetchRelayTable , relayAmountWidget @@ -50,7 +49,7 @@ delegateWindow eOpen dWallet dRelayNames = mdo divClass "dao-DelegateWindow_EnterUrl" $ text "Choose a relay URL above or enter a new one below:" - dInputText <- inputWidget eOpen + dInputText <- viewDelegateInput eOpen let eInputText = updated dInputText let eNonEmptyUrl = ffilter (not . T.null) eInputText @@ -63,7 +62,7 @@ delegateWindow eOpen dWallet dRelayNames = mdo ] dIsInvalidUrl <- holdDyn UrlEmpty eUrlStatus - (eStake, eUnstake) <- stakingButtonWidget dIsInvalidUrl + (eStake, eUnstake) <- viewStakingButton dIsInvalidUrl let eUrlStake = tagPromptlyDyn dInputText eStake let eUrlUnstake = unStakeUrl <$ eUnstake @@ -75,11 +74,11 @@ delegateWindow eOpen dWallet dRelayNames = mdo return eUrl pure () -inputWidget :: +viewDelegateInput :: (MonadWidget t m) => Event t () -> m (Dynamic t Text) -inputWidget eOpen = divClass "w-row" $ do +viewDelegateInput eOpen = divClass "w-row" $ do inp <- inputElement $ def @@ -93,11 +92,11 @@ inputWidget eOpen = divClass "w-row" $ do setFocusDelayOnEvent inp eOpen return $ value inp -stakingButtonWidget :: +viewStakingButton :: (MonadWidget t m) => Dynamic t UrlStatus -> m (Event t (), Event t ()) -stakingButtonWidget dUrlStatus = +viewStakingButton dUrlStatus = divClass "dao-DelegateWindow_ButtonStatusContainer" $ do -- The Stake button disable with invalid url and performant status. eStake <- diff --git a/frontend/src/ENCOINS/DAO/Widgets/RelayTable.hs b/frontend/src/ENCOINS/DAO/Widgets/DelegateWindow/RelayTable.hs similarity index 74% rename from frontend/src/ENCOINS/DAO/Widgets/RelayTable.hs rename to frontend/src/ENCOINS/DAO/Widgets/DelegateWindow/RelayTable.hs index 6a0d181a..7fe128aa 100644 --- a/frontend/src/ENCOINS/DAO/Widgets/RelayTable.hs +++ b/frontend/src/ENCOINS/DAO/Widgets/DelegateWindow/RelayTable.hs @@ -2,7 +2,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE RecursiveDo #-} -module ENCOINS.DAO.Widgets.RelayTable +module ENCOINS.DAO.Widgets.DelegateWindow.RelayTable ( fetchDelegatedByAddress , fetchRelayNames , fetchRelayTable @@ -22,10 +22,11 @@ import Reflex.Dom import Backend.Protocol.Types import Backend.Servant.Requests (infoRequestWrapper, serversRequestWrapper) -import Backend.Utility (switchHoldDyn, toText) +import Common.Reflex.Extra (switchHoldDyn) +import Common.Url (stripHostOrRelay) +import Common.Utility (toText) import Config.Config (delegateServerUrl) -import ENCOINS.Common.Events -import ENCOINS.Common.Utils (stripHostOrRelay) +import Common.Events import ENCOINS.Common.Widgets.Basic (btnWithBlock) relayAmountWidget :: @@ -57,34 +58,21 @@ relayAmountWidget eeRelays emDelegated dRelayNames = do pure never else do let normalAmount = normalizeAmount amount - rainbowTr normalAmount $ do - tdRelay $ dynText $ fromMaybe relay . Map.lookup relay <$> dRelayNames - tdAmount $ text $ mkAmount normalAmount - eClick <- - tdButton $ - btnWithBlock - "button-switching inverted" - "" - (isDelegated relay <$> dmDelegated) - (dynText $ mkDelegateButton relay <$> dmDelegated) - pure $ relay <$ eClick + let dRelayName = fromMaybe relay . Map.lookup relay <$> dRelayNames + let dDelegateBlock = isDelegated relay <$> dmDelegated + let dDelegateTag = dynText $ delegationButtonText relay <$> dmDelegated + ev <- + viewDelegateRow + normalAmount + dRelayName + dDelegateBlock + dDelegateTag + pure $ relay <$ ev pure $ leftmost evs where article = elAttr "article" ("class" =: "dao-DelegateWindow_TableWrapper") table = elAttr "table" ("class" =: "dao-DelegateWindow_Table") - tr = elAttr "tr" ("class" =: "dao-DelegateWindow_TableRow") - trRed = elAttr "tr" ("class" =: "dao-DelegateWindow_TableRow-red") - trYellow = elAttr "tr" ("class" =: "dao-DelegateWindow_TableRow-yellow") - trGreen = elAttr "tr" ("class" =: "dao-DelegateWindow_TableRow-green") th = elAttr "th" ("class" =: "dao-DelegateWindow_TableHeader") - tdRelay = elAttr "td" ("class" =: "dao-DelegateWindow_TableRelay") - tdAmount = elAttr "td" ("class" =: "dao-DelegateWindow_TableAmount") - tdButton = elAttr "td" ("class" =: "dao-DelegateWindow_TableButton") - rainbowTr stakedAmount - | stakedAmount > 100000 = trRed - | stakedAmount <= 100000 && stakedAmount > 90000 = trYellow - | stakedAmount <= 90000 && stakedAmount > 50000 = trGreen - | otherwise = tr fetchRelayTable :: (MonadWidget t m) => @@ -112,9 +100,11 @@ mkAmount :: Integer -> Text mkAmount amount = toText amount <> " ENCS" -mkDelegateButton :: Text -> Maybe (Text, Integer) -> Text -mkDelegateButton relay = - maybe "Delegate" (\(r, n) -> bool "Delegate" (mkAmount $ normalizeAmount n) (r == relay)) +delegationButtonText :: Text -> Maybe (Text, Integer) -> Text +delegationButtonText relay = + maybe + "Delegate" + (\(r, n) -> bool "Delegate" (mkAmount $ normalizeAmount n) (r == relay)) isDelegated :: Text -> Maybe (Text, Integer) -> Bool isDelegated relay = \case @@ -154,3 +144,38 @@ fetchRelayNames eOpen = do e <- newEvent pure $ names <$ e holdDyn Map.empty eNames + +viewDelegateRow :: + (MonadWidget t m) => + Integer + -> Dynamic t Text + -> Dynamic t Bool + -> m () + -> m (Event t ()) +viewDelegateRow normalAmount dRelayName dDelegateBlock dDelegateTag = + rainbowTr normalAmount $ do + tdRelay $ dynText dRelayName + tdAmount $ text $ mkAmount normalAmount + eClick <- + tdButton $ + btnWithBlock + "button-switching inverted" + "" + dDelegateBlock + dDelegateTag + pure eClick + where + trRed = elAttr "tr" ("class" =: "dao-DelegateWindow_TableRow-red") + trYellow = elAttr "tr" ("class" =: "dao-DelegateWindow_TableRow-yellow") + trGreen = elAttr "tr" ("class" =: "dao-DelegateWindow_TableRow-green") + tdRelay = elAttr "td" ("class" =: "dao-DelegateWindow_TableRelay") + tdAmount = elAttr "td" ("class" =: "dao-DelegateWindow_TableAmount") + tdButton = elAttr "td" ("class" =: "dao-DelegateWindow_TableButton") + rainbowTr stakedAmount + | stakedAmount > 100000 = trRed + | stakedAmount <= 100000 && stakedAmount > 90000 = trYellow + | stakedAmount <= 90000 && stakedAmount > 50000 = trGreen + | otherwise = tr + +tr :: (DomBuilder t m) => m a -> m a +tr = elAttr "tr" ("class" =: "dao-DelegateWindow_TableRow") diff --git a/frontend/src/ENCOINS/DAO/Widgets/Navbar.hs b/frontend/src/ENCOINS/DAO/Widgets/Navbar.hs index 08be9305..ade0aebb 100755 --- a/frontend/src/ENCOINS/DAO/Widgets/Navbar.hs +++ b/frontend/src/ENCOINS/DAO/Widgets/Navbar.hs @@ -10,7 +10,7 @@ import Backend.Wallet (Wallet (..)) import Config.Config (NetworkConfig (dao), NetworkId (..), networkConfig) import ENCOINS.Common.Widgets.Basic (btnWithBlock, logo) import ENCOINS.Common.Widgets.Connect (connectWidget) -import ENCOINS.Common.Widgets.MoreMenu (NavMoreMenuClass (..), moreMenuWidget) +import ENCOINS.Common.Widgets.MoreMenu (NavMoreMenuClass (..), viewMoreMenu) data Dao = Connect | Delegate | MoreMenu deriving (Eq, Show) @@ -54,7 +54,7 @@ navbarWidget w dIsBlocked dIsBlockedConnect = do dIsBlocked (text "DELEGATE") eMore <- - moreMenuWidget + viewMoreMenu (NavMoreMenuClass "common-Nav_Container_MoreMenu" "common-Nav_MoreMenu") pure $ leftmost [Connect <$ eConnect, Delegate <$ eDelegate, MoreMenu <$ eMore] diff --git a/frontend/src/ENCOINS/DAO/PollResults.hs b/frontend/src/ENCOINS/DAO/Widgets/Poll/PollResults.hs similarity index 99% rename from frontend/src/ENCOINS/DAO/PollResults.hs rename to frontend/src/ENCOINS/DAO/Widgets/Poll/PollResults.hs index 7b279b4d..ca247512 100644 --- a/frontend/src/ENCOINS/DAO/PollResults.hs +++ b/frontend/src/ENCOINS/DAO/Widgets/Poll/PollResults.hs @@ -1,7 +1,7 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE QuasiQuotes #-} -module ENCOINS.DAO.PollResults where +module ENCOINS.DAO.Widgets.Poll.PollResults where import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Text (Text) diff --git a/frontend/src/ENCOINS/DAO/Polls.hs b/frontend/src/ENCOINS/DAO/Widgets/Poll/Polls.hs similarity index 98% rename from frontend/src/ENCOINS/DAO/Polls.hs rename to frontend/src/ENCOINS/DAO/Widgets/Poll/Polls.hs index 3969f6fd..dc251486 100755 --- a/frontend/src/ENCOINS/DAO/Polls.hs +++ b/frontend/src/ENCOINS/DAO/Widgets/Poll/Polls.hs @@ -1,4 +1,4 @@ -module ENCOINS.DAO.Polls where +module ENCOINS.DAO.Widgets.Poll.Polls where import Data.IntMap.Strict (IntMap, fromList, mapEither) import Data.Text (Text) @@ -12,9 +12,9 @@ import Data.Time ) import Reflex.Dom -import Backend.Utility (column, space, toText) +import Common.Utility (column, space, toText) import ENCOINS.Common.Widgets.Basic (br, lnkInline) -import ENCOINS.DAO.PollResults +import ENCOINS.DAO.Widgets.Poll.PollResults data Poll m = Poll { pollNumber :: Int diff --git a/frontend/src/ENCOINS/DAO/Widgets/PollWidget.hs b/frontend/src/ENCOINS/DAO/Widgets/PollWidget.hs index 7b8f1c53..2ded63ea 100755 --- a/frontend/src/ENCOINS/DAO/Widgets/PollWidget.hs +++ b/frontend/src/ENCOINS/DAO/Widgets/PollWidget.hs @@ -1,17 +1,29 @@ module ENCOINS.DAO.Widgets.PollWidget where +import Control.Lens ((^.)) +import Data.ByteString (ByteString) import Data.Text (Text, pack) import Data.Text.Encoding (encodeUtf8) +import Data.Time + ( UTCTime + ) +import qualified Foreign.JavaScript.Utils as Utils +import GHCJS.DOM.Blob (newBlob) +import qualified GHCJS.DOM.Document as D +import GHCJS.DOM.Element (setAttribute) +import qualified GHCJS.DOM.HTMLElement as DOMHtml +import GHCJS.DOM.Types hiding (ByteString, Event, Text, toText) +import GHCJS.DOM.URL (createObjectURL, revokeObjectURL) +import qualified Language.Javascript.JSaddle as JS import Reflex.Dom -import Text.Printf +import Text.Printf (printf) -import Backend.Utility (formatPollTime, toText) import Backend.Wallet (LucidConfig (..), Wallet (..), lucidConfigDao, toJS) -import ENCOINS.App.Widgets.Basic (elementResultJS) -import ENCOINS.Common.Utils (downloadVotes, toJsonStrict) +import Common.Reflex.Dom.Extra (elementResultJS) +import Common.Utility (formatPollTime, toJsonStrict, toText) import ENCOINS.Common.Widgets.Basic (btn, btnWithBlock) -import ENCOINS.DAO.PollResults -import ENCOINS.DAO.Polls (Poll (..)) +import ENCOINS.DAO.Widgets.Poll.PollResults (VoteResult (..)) +import ENCOINS.DAO.Widgets.Poll.Polls (Poll (..)) import ENCOINS.Website.Widgets.Basic (container) import JS.DAO (daoPollVoteTx) @@ -22,79 +34,98 @@ pollWidget :: -> Poll m -> m () pollWidget dWallet dIsBlocked (Poll n question summary answers' _ endTime) = do - explainer question summary - + viewPollExplainer question summary endTime let answers = fmap fst $ mkVoteList answers' - container "" $ do - es <- - mapM - ( btnWithBlock + eAnswers <- do + let viewPollButton answer = + btnWithBlock "button-switching dao-Poll_Button" "" dIsBlocked - . text - ) - answers - let e = leftmost $ zipWith (<$) answers es - let LucidConfig apiKey networkId policyId assetName = lucidConfigDao - performEvent_ $ - daoPollVoteTx n apiKey networkId policyId assetName - <$> attachPromptlyDyn (fmap (toJS . walletName) dWallet) e + $ text answer + container "" $ mapM viewPollButton answers + let eAnswer = leftmost $ zipWith (<$) answers eAnswers + let LucidConfig apiKey networkId policyId assetName = lucidConfigDao + performEvent_ $ + daoPollVoteTx n apiKey networkId policyId assetName + <$> attachPromptlyDyn (fmap (toJS . walletName) dWallet) eAnswer - dMsg <- elementResultJS ("elementPoll" <> toText n) id - container "" $ divClass "app-text-normal" $ dynText dMsg - where - -- TODO: make this a widget - explainer tagsTitle tagsExplainer = container "" $ divClass "div-explainer" $ do - elAttr "h4" ("class" =: "h4" <> "style" =: "margin-bottom: 30px;") tagsTitle - elAttr - "p" - ("class" =: "p-explainer" <> "style" =: "text-align: justify;") - tagsExplainer - divClass "app-text-small" $ - text $ - "The vote ends on " <> formatPollTime endTime <> "." + dMsg <- elementResultJS ("elementPoll" <> toText n) id + container "" $ divClass "app-text-normal" $ dynText dMsg pollCompletedWidget :: (MonadWidget t m) => Poll m -> m () pollCompletedWidget (Poll n question summary voteResults fullAnswers endTime) = do - explainer question summary + viewPollExplainer question summary endTime container "" $ mapM_ ( \(a, r) -> btn "vote-option-result" - "margin-left: 30px; margin-right: 30px; margin-bottom: 20px;" $ do - text a - elAttr "div" ("style" =: "margin-right: 10px; margin-left: 10px;") blank - text r + "margin-left: 30px; margin-right: 30px; margin-bottom: 20px;" + $ do + text a + elAttr "div" ("style" =: "margin-right: 10px; margin-left: 10px;") blank + text r ) $ mkVoteList voteResults - container "" $ - elAttr + container "" + $ elAttr "div" ( "class" =: "h5" <> "style" =: "-webkit-filter: brightness(35%); filter: brightness(35%);" - ) $ - text "Download poll results" - container "" $ + ) + $ text "Download poll results" + eDownload <- container "" $ divClass "dao-VoteDownload" $ do - eDownload <- btn "button-switching flex-center" "" $ text "DOWNLOAD" - downloadVotes (toJsonStrict voteResults) "result" n eDownload - downloadVotes (encodeUtf8 fullAnswers) "result_full" n eDownload - where - -- TODO: make this a widget - explainer tagsTitle tagsExplainer = container "" $ divClass "div-explainer" $ do - elAttr "h4" ("class" =: "h4" <> "style" =: "margin-bottom: 30px;") tagsTitle - elAttr - "p" - ("class" =: "p-explainer" <> "style" =: "text-align: justify;") - tagsExplainer - divClass "app-text-small" $ - text $ - "The vote ended on " <> formatPollTime endTime <> "." + btn "button-switching flex-center" "" $ text "DOWNLOAD" + + downloadVotes (toJsonStrict voteResults) "result" n eDownload + downloadVotes (encodeUtf8 fullAnswers) "result_full" n eDownload mkVoteList :: VoteResult -> [(Text, Text)] mkVoteList (VoteResult yes no) = [ ("Yes", pack $ printf "%.2f%%" yes) , ("No", pack $ printf "%.2f%%" no) ] + +viewPollExplainer :: (MonadWidget t m) => m () -> m () -> UTCTime -> m () +viewPollExplainer tagsTitle tagsExplainer endTime = container "" $ + divClass "div-explainer" $ do + elAttr "h4" ("class" =: "h4" <> "style" =: "margin-bottom: 30px;") tagsTitle + elAttr + "p" + ("class" =: "p-explainer" <> "style" =: "text-align: justify;") + tagsExplainer + divClass "app-text-small" $ + text $ + "The vote ends on " <> formatPollTime endTime <> "." + +triggerDownload :: + (MonadJSM m) => + Document + -> Text + -- ^ mime type + -> Text + -- ^ file name + -> ByteString + -- ^ content + -> m () +triggerDownload doc mime filename s = do + t <- Utils.bsToArrayBuffer s + o <- JS.liftJSM $ JS.obj ^. JS.jss ("type" :: Text) (mime :: Text) + options <- JS.liftJSM $ BlobPropertyBag <$> JS.toJSVal o + blob <- newBlob [t] (Just options) + (url :: Text) <- createObjectURL blob + a <- D.createElement doc ("a" :: Text) + setAttribute a ("style" :: Text) ("display: none;" :: Text) + setAttribute a ("download" :: Text) filename + setAttribute a ("href" :: Text) url + DOMHtml.click $ DOMHtml.HTMLElement $ unElement a + revokeObjectURL url + +downloadVotes :: + (MonadWidget t m) => ByteString -> Text -> Int -> Event t () -> m () +downloadVotes txt name num e = do + doc <- askDocument + performEvent_ $ ffor e $ \_ -> + triggerDownload doc "application/json" (name <> toText num <> ".json") txt diff --git a/frontend/src/ENCOINS/DAO/Widgets/StatusWidget.hs b/frontend/src/ENCOINS/DAO/Widgets/StatusWidget.hs index c392a6a0..42e2274b 100644 --- a/frontend/src/ENCOINS/DAO/Widgets/StatusWidget.hs +++ b/frontend/src/ENCOINS/DAO/Widgets/StatusWidget.hs @@ -18,7 +18,7 @@ import Backend.Status , isWalletError , textDaoStatus ) -import Backend.Utility (space, toText) +import Common.Utility (space, toText) import Backend.Wallet ( LucidConfig (..) , Wallet (..) @@ -28,9 +28,10 @@ import Backend.Wallet , lucidConfigDao ) import Config.Config (NetworkConfig (dao), networkConfig) -import ENCOINS.App.Widgets.Basic (elementResultJS, walletError) -import ENCOINS.Common.Events -import ENCOINS.Common.Widgets.Advanced (foldDynamicAny) +import ENCOINS.Common.Widgets.Advanced (walletError) +import Common.Events +import Common.Reflex.Extra (foldDynamicAny) +import Common.Reflex.Dom.Extra (elementResultJS) handleStatus :: (MonadWidget t m) => diff --git a/frontend/src/ENCOINS/Website/Widgets/LandingPage.hs b/frontend/src/ENCOINS/Website/Widgets/LandingPage.hs index 1c12f07c..42eb5448 100755 --- a/frontend/src/ENCOINS/Website/Widgets/LandingPage.hs +++ b/frontend/src/ENCOINS/Website/Widgets/LandingPage.hs @@ -8,7 +8,7 @@ import Data.Text (Text) import Reflex.Dom import Reflex.ScriptDependent (widgetHoldUntilDefined) -import ENCOINS.Common.Events +import Common.Events import ENCOINS.Common.Widgets.Basic import ENCOINS.Website.Widgets.Basic import ENCOINS.Website.Widgets.Resourses (ourResourses) diff --git a/frontend/test/Spec.hs b/frontend/test/Spec.hs index 9766a225..b6ce2567 100644 --- a/frontend/test/Spec.hs +++ b/frontend/test/Spec.hs @@ -8,7 +8,7 @@ import Data.Either (isLeft) import Data.Text import qualified Data.Text as T import qualified Data.Text.Encoding as TE -import ENCOINS.Common.Utils (checkUrl, stripHost) +import Common.Url (checkUrl, stripHost) import Test.Hspec (Spec, describe, hspec, it, shouldBe, shouldSatisfy) main :: IO () diff --git a/result/css/encoins.webflow.css b/result/css/encoins.webflow.css index a70664a2..b04635a8 100644 --- a/result/css/encoins.webflow.css +++ b/result/css/encoins.webflow.css @@ -1004,6 +1004,26 @@ body { color: #000; } +.app-DialogWindow_EnterPassword { + width: min(90%, 750px); + padding-left: min(5%, 70px); + padding-right: min(5%, 70px); + padding-top: min(5%, 30px); + padding-bottom: min(5%, 30px); + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: stretch; + -webkit-align-items: stretch; + -ms-flex-align: stretch; + align-items: stretch; + border-radius: 37px; + background-color: #fff; + color: #000; +} + .dialog-window-title { font-size: 24px; display: -webkit-box; @@ -1047,6 +1067,54 @@ body { backdrop-filter: brightness(30%); } +.app-EnterPasswordWindow { + position: fixed; + left: 0%; + top: 0%; + right: 0%; + bottom: 0%; + z-index: 1000; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + -webkit-backdrop-filter: brightness(30%); + backdrop-filter: brightness(30%); + flex-direction: column; +} + +.app-EnterPasswordWindow-none { + position: fixed; + left: 0%; + top: 0%; + right: 0%; + bottom: 0%; + z-index: 1000; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: none; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + -webkit-backdrop-filter: brightness(30%); + backdrop-filter: brightness(30%); + flex-direction: column; +} + .connect-title-div { display: -webkit-box; display: -webkit-flex; @@ -1873,6 +1941,8 @@ body { color: #000; position: static; display: block; + border-top-left-radius: 0px; + border-top-right-radius: 0px; } @media screen and (max-width: 767px) { @@ -2610,4 +2680,67 @@ body { justify-content: left; gap: 10px; margin-top: 10px; +} + +.app-PasswordError_Container { + display: flex; + justify-content: space-between; +} + +.app-PasswordError_Message { + display: flex; + color: #ea384c; + align-items: center; +} + +.app-Password_InputTitle { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: left; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + font-family: Poppins, sans-serif; + font-size: 18px; + line-height: 32px; +} + +.app-SendButton_Tooltip_TxInvalid { + display: block; + position: static; + z-index: 500; + padding: 10px 20px; + border-radius: 6px; + background-color: #fff; + -webkit-filter: brightness(75%); + filter: brightness(75%); + color: #000; + transition: all 1s ease; + border-top-left-radius: 0px; + border-top-right-radius: 0px; +} + +.app-Transfer_SendToWalletWindow_Secret { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + font-family: Poppins, sans-serif; + font-size: 18px; + line-height: 32px; + justify-content: space-between; + text-align:left; } \ No newline at end of file diff --git a/script/build.sh b/script/build.sh index 291612dc..e6dc4444 100755 --- a/script/build.sh +++ b/script/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -source ./script/utils.sh +source ./script/common.sh version=$(get_version) printf "Current frontend version: %s" "$version" diff --git a/script/build_and_copy.sh b/script/build_and_copy.sh index 8de921ee..eb38d066 100755 --- a/script/build_and_copy.sh +++ b/script/build_and_copy.sh @@ -1,6 +1,6 @@ #!/bin/bash -source ./script/utils.sh +source ./script/common.sh version=$(get_version) printf "Current frontend version: %s" "$version" diff --git a/script/build_dev_js.sh b/script/build_dev_js.sh index d843b8bd..facf5afa 100755 --- a/script/build_dev_js.sh +++ b/script/build_dev_js.sh @@ -1,6 +1,6 @@ #!/bin/bash -source ./script/utils.sh +source ./script/common.sh version=$(get_version) printf "Current frontend version: %s" "$version" diff --git a/script/build_html.sh b/script/build_html.sh index c7c59036..3c9e7227 100755 --- a/script/build_html.sh +++ b/script/build_html.sh @@ -1,5 +1,5 @@ #!/bin/bash -source ./script/utils.sh +source ./script/common.sh build_html \ No newline at end of file diff --git a/script/build_js.sh b/script/build_js.sh index 25e405ae..a7dbd5ee 100755 --- a/script/build_js.sh +++ b/script/build_js.sh @@ -1,6 +1,6 @@ #!/bin/bash -source ./script/utils.sh +source ./script/common.sh version=$(get_version) printf "Current frontend version: %s" "$version" diff --git a/script/utils.sh b/script/common.sh similarity index 100% rename from script/utils.sh rename to script/common.sh diff --git a/script/dev_down.sh b/script/dev_down.sh index 97689a9f..071d4eaf 100755 --- a/script/dev_down.sh +++ b/script/dev_down.sh @@ -12,6 +12,5 @@ tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".0 C-c ; tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".1 C-c ; tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".2 C-c ; tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".3 C-c ; -tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".4 C-c ; tmux kill-session -t "$FRONT_SESSION" \ No newline at end of file diff --git a/script/dev_up.sh b/script/dev_up.sh index 1c8380b4..18259e89 100755 --- a/script/dev_up.sh +++ b/script/dev_up.sh @@ -30,7 +30,6 @@ tmux new-window -t "$FRONT_SESSION":1 -n "$WINDOW_APPS" tmux split-window -h -t "$FRONT_SESSION":"$WINDOW_APPS".0 tmux split-window -v -t "$FRONT_SESSION":"$WINDOW_APPS".0 -tmux split-window -v -t "$FRONT_SESSION":"$WINDOW_APPS".0 tmux split-window -v -t "$FRONT_SESSION":"$WINDOW_APPS".1 tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".0 "cd $TOOL_APP" C-m; @@ -41,20 +40,15 @@ tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".1 "cd $TOOL_APP" C-m; tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".1 "clear" C-m ; tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".1 "encoins-cloud" C-m; -tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".2 "cd $TOOL_APP" C-m; +tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".2 "cd $HOST_FRONTEND" C-m; tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".2 "clear" C-m ; -tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".2 "encoins --run"; +tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".2 "./script/run.sh " C-m; -tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".3 "cd $HOST_FRONTEND" C-m; +tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".3 "cd $TOOL_APP" C-m; tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".3 "clear" C-m ; -tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".3 "./script/run.sh "; - -tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".4 "cd $HOST_FRONTEND" C-m; -tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".4 "clear" C-m ; -tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".4 "./script/docker_dev_run.sh" C-m ; -tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".4 "./script/build_dev_js.sh" C-m ; +tmux send-keys -t "$FRONT_SESSION":"$WINDOW_APPS".3 "encoins --run"; -tmux select-pane -t "$FRONT_SESSION":"$WINDOW_APPS".2 +tmux select-pane -t "$FRONT_SESSION":"$WINDOW_APPS".3 tmux select-window -t "$FRONT_SESSION":"$WINDOW_CARDANO".2 # Attach to the session diff --git a/script/run.sh b/script/run.sh index 3dfa326e..722b3fcd 100755 --- a/script/run.sh +++ b/script/run.sh @@ -1,6 +1,6 @@ #!/bin/bash -source ./script/utils.sh +source ./script/common.sh version=$(get_version) printf "Current frontend version: %s" "$version"