Updated on 2026-08-14

This commit is contained in:
Tangem 2021-04-16 07:37:39 +00:00
commit 554456e809
52 changed files with 1709 additions and 518 deletions

View file

@ -0,0 +1,914 @@
[
{
"decimalCount" : 18,
"symbol" : "ZRX",
"name" : "0x",
"contractAddress" : "0xE41d2489571d322189246DaFA5ebDe1F4699F498"
},
{
"decimalCount" : 18,
"symbol" : "AAVE",
"name" : "Aave",
"contractAddress" : "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9"
},
{
"decimalCount" : 18,
"symbol" : "LEND",
"name" : "Aave (OLD)",
"contractAddress" : "0x80fB784B7eD66730e8b1DBd9820aFD29931aab03"
},
{
"decimalCount" : 18,
"symbol" : "ADX",
"name" : "AdEx",
"contractAddress" : "0xADE00C28244d5CE17D72E40330B1c318cD12B7c3"
},
{
"decimalCount" : 9,
"symbol" : "ADT",
"name" : "adToken",
"contractAddress" : "0xD0D6D6C5Fe4a677D343cC433536BB717bAe167dD"
},
{
"decimalCount" : 18,
"symbol" : "ELF",
"name" : "Aelf",
"contractAddress" : "0xbf2179859fc6d5bee9bf9158632dc51678a4100e"
},
{
"decimalCount" : 8,
"symbol" : "ARN",
"name" : "Aeron",
"contractAddress" : "0xBA5F11b16B155792Cf3B2E6880E8706859A8AEB6"
},
{
"decimalCount" : 18,
"symbol" : "AE",
"name" : "Aeternity",
"contractAddress" : "0x5ca9a71b1d01849c0a95490cc00559717fcf0d1d"
},
{
"decimalCount" : 8,
"symbol" : "AION",
"name" : "Aion",
"contractAddress" : "0x4CEdA7906a5Ed2179785Cd3A40A69ee8bc99C466"
},
{
"decimalCount" : 4,
"symbol" : "AST",
"name" : "Airswap",
"contractAddress" : "0x27054b13b1b798b345b591a4d22e6562d47ea75a"
},
{
"decimalCount" : 18,
"symbol" : "AMB",
"name" : "Amber",
"contractAddress" : "0x4dc3643dbc642b72c158e7f3d2ff232df61cb6ce"
},
{
"decimalCount" : 18,
"symbol" : "AMLT",
"name" : "AMLT by Coinfirm",
"contractAddress" : "0xca0e7269600d353f70b14ad118a49575455c0f2f"
},
{
"decimalCount" : 18,
"symbol" : "APPC",
"name" : "AppCoins",
"contractAddress" : "0x1a7a8bd9106f2b8d977e08582dc7d24c723ab0db"
},
{
"decimalCount" : 18,
"symbol" : "ANT",
"name" : "Aragon",
"contractAddress" : "0xa117000000f279d81a1d3cc75430faa017fa5a2e"
},
{
"decimalCount" : 18,
"symbol" : "ABT",
"name" : "ArcBlock",
"contractAddress" : "0xb98d4c97425d9908e66e53a6fdf673acca0be986"
},
{
"decimalCount" : 18,
"symbol" : "ATMI",
"name" : "Atonomi",
"contractAddress" : "0x97aeb5066e1a590e868b511457beb6fe99d329f5"
},
{
"decimalCount" : 18,
"symbol" : "REP",
"name" : "Augur",
"contractAddress" : "0x1985365e9f78359a9B6AD760e32412f4a445E862"
},
{
"decimalCount" : 18,
"symbol" : "AVM",
"name" : "AVM Ecosystem",
"contractAddress" : "0x74004a7227615fb52b82d17ffabfa376907d8a4d"
},
{
"decimalCount" : 18,
"symbol" : "BNT",
"name" : "Bancor",
"contractAddress" : "0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C"
},
{
"decimalCount" : 18,
"symbol" : "BAT",
"name" : "Basic Attention",
"contractAddress" : "0x0D8775F648430679A709E98d2b0Cb6250d2887EF"
},
{
"decimalCount" : 18,
"symbol" : "BNB",
"name" : "Binance Coin",
"contractAddress" : "0xb8c77482e45f1f44de1745f52c74426c631bdd52"
},
{
"decimalCount" : 18,
"symbol" : "BUSD",
"name" : "Binance USD",
"contractAddress" : "0x4fabb145d64652a948d72533023f6e7a623c7c53"
},
{
"decimalCount" : 18,
"symbol" : "STU",
"name" : "bitJob",
"contractAddress" : "0x0371a82e4a9d0a4312f3ee2ac9c6958512891372"
},
{
"decimalCount" : 18,
"symbol" : "BIX",
"name" : "BIX Token",
"contractAddress" : "0xb3104b4b9da82025e8b9f8fb28b3553ce2f67069"
},
{
"decimalCount" : 18,
"symbol" : "VEE",
"name" : "BLOCKv",
"contractAddress" : "0x340d2bde5eb28c1eed91b2f790723e3b160613b7"
},
{
"decimalCount" : 18,
"symbol" : "BLZ",
"name" : "Bluzelle",
"contractAddress" : "0x5732046a883704404f284ce41ffadd5b007fd668"
},
{
"decimalCount" : 18,
"symbol" : "BRD",
"name" : "BRD Token",
"contractAddress" : "0x558ec3152e2eb2174905cd19aea4e34a23de9ad6"
},
{
"decimalCount" : 8,
"symbol" : "BTM",
"name" : "Bytom",
"contractAddress" : "0xcb97e65f07da24d46bcdd078ebebd7c6e6e3d750"
},
{
"decimalCount" : 18,
"symbol" : "CAS",
"name" : "Cashaa",
"contractAddress" : "0xe8780b48bdb05f928697a5e8155f672ed91462f7"
},
{
"decimalCount" : 18,
"symbol" : "CSP",
"name" : "Caspian",
"contractAddress" : "0xa6446d655a0c34bc4f05042ee88170d056cbaf45"
},
{
"decimalCount" : 18,
"symbol" : "LINK",
"name" : "ChainLink",
"contractAddress" : "0x514910771af9ca656af840dff83e8264ecf986ca"
},
{
"decimalCount" : 8,
"symbol" : "TIME",
"name" : "Chronobank",
"contractAddress" : "0x6531f133e6DeeBe7F2dcE5A0441aA7ef330B4e53"
},
{
"decimalCount" : 18,
"symbol" : "CND",
"name" : "Cindicator",
"contractAddress" : "0xd4c435f5b09f855c3317c8524cb1f586e42795fa"
},
{
"decimalCount" : 8,
"symbol" : "CVC",
"name" : "Civic",
"contractAddress" : "0x41e5560054824ea6b0732e656e3ad64e20e94e45"
},
{
"decimalCount" : 18,
"symbol" : "CNN",
"name" : "CNN Token",
"contractAddress" : "0x8713d26637cf49e1b6b4a7ce57106aabc9325343"
},
{
"decimalCount" : 18,
"symbol" : "CFI",
"name" : "Cofound.it",
"contractAddress" : "0x12FEF5e57bF45873Cd9B62E9DBd7BFb99e32D73e"
},
{
"decimalCount" : 18,
"symbol" : "CCC",
"name" : "Container Crypto Coin",
"contractAddress" : "0x9e3359f862b6c7f5c660cfd6d1aa6909b1d9504d"
},
{
"decimalCount" : 6,
"symbol" : "CS",
"name" : "Credits",
"contractAddress" : "0x46b9ad944d1059450da1163511069c718f699d31"
},
{
"decimalCount" : 18,
"symbol" : "CRPT",
"name" : "Crypterium",
"contractAddress" : "0x08389495d7456e1951ddf7c3a1314a4bfb646d8b"
},
{
"decimalCount" : 18,
"symbol" : "AUTO",
"name" : "CUBE",
"contractAddress" : "0x622dFfCc4e83C64ba959530A5a5580687a57581b"
},
{
"decimalCount" : 18,
"symbol" : "CMT",
"name" : "CyberMiles",
"contractAddress" : "0xf85fEea2FdD81d51177F6b8F35F0e6734Ce45F5F"
},
{
"decimalCount" : 18,
"symbol" : "DAI",
"name" : "Dai",
"contractAddress" : "0x6b175474e89094c44da98b954eedeac495271d0f"
},
{
"decimalCount" : 18,
"symbol" : "MANA",
"name" : "Decentraland",
"contractAddress" : "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942"
},
{
"decimalCount" : 8,
"symbol" : "DENT",
"name" : "DENT",
"contractAddress" : "0x3597bfd533a99c9aa083587b074434e61eb0a258"
},
{
"decimalCount" : 0,
"symbol" : "XJP",
"name" : "Digital JPY",
"contractAddress" : "0x39689fE671C01fcE173395f6BC45D4C332026666"
},
{
"decimalCount" : 9,
"symbol" : "DGD",
"name" : "DigixDAO",
"contractAddress" : "0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A"
},
{
"decimalCount" : 18,
"symbol" : "DNT",
"name" : "district0x",
"contractAddress" : "0x0abdace70d3790235af448c88547603b945604ea"
},
{
"decimalCount" : 18,
"symbol" : "DOV",
"name" : "Dovu",
"contractAddress" : "0xac3211a5025414af2866ff09c23fc18bc97e79b1"
},
{
"decimalCount" : 18,
"symbol" : "DRGN",
"name" : "Dragon",
"contractAddress" : "0x419c4db4b9e25d6db2ad9691ccb832c8d9fda05e"
},
{
"decimalCount" : 0,
"symbol" : "EDG",
"name" : "Edgeless",
"contractAddress" : "0x08711D3B02C8758F2FB3ab4e80228418a7F8e39c"
},
{
"decimalCount" : 18,
"symbol" : "EDO",
"name" : "Eidoo",
"contractAddress" : "0xced4e93198734ddaff8492d525bd258d49eb388e"
},
{
"decimalCount" : 8,
"symbol" : "ENG",
"name" : "Enigma",
"contractAddress" : "0xf0ee6b27b759c9893ce4f094b49ad28fd15a23e4"
},
{
"decimalCount" : 18,
"symbol" : "ENJ",
"name" : "EnjinCoin",
"contractAddress" : "0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c"
},
{
"decimalCount" : 18,
"symbol" : "EOS",
"name" : "EOS",
"contractAddress" : "0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0"
},
{
"decimalCount" : 2,
"symbol" : "EUR.AVM",
"name" : "EUR AVM",
"contractAddress" : "0xF01Cd2f1c9E42c509d309aC8C5b29B6dA8E64b1a"
},
{
"decimalCount" : 18,
"symbol" : "XUC",
"name" : "Exchange Union",
"contractAddress" : "0xc324a2f6b05880503444451b8b27e6f9e63287cb"
},
{
"decimalCount" : 18,
"symbol" : "1ST",
"name" : "FirstBlood",
"contractAddress" : "0xAf30D2a7E90d7DC361c8C4585e9BB7D2F6f15bc7"
},
{
"decimalCount" : 18,
"symbol" : "FXC",
"name" : "Flexacoin",
"contractAddress" : "0x4a57E687b9126435a9B19E4A802113e266AdeBde"
},
{
"decimalCount" : 8,
"symbol" : "FUN",
"name" : "FunFair",
"contractAddress" : "0x419D0d8BdD9aF5e606Ae2232ed285Aff190E711b"
},
{
"decimalCount" : 18,
"symbol" : "FSN",
"name" : "Fusion",
"contractAddress" : "0xd0352a019e9ab9d757776f532377aaebd36fd541"
},
{
"decimalCount" : 2,
"symbol" : "GUSD",
"name" : "Gemini Dollar",
"contractAddress" : "0x056fd409e1d7a124bd7017459dfea2f387b6d5cd"
},
{
"decimalCount" : 9,
"symbol" : "GNX",
"name" : "Genaro Network",
"contractAddress" : "0x6ec8a24cabdc339a06a172f8223ea557055adaa5"
},
{
"decimalCount" : 18,
"symbol" : "GVT",
"name" : "Genesis Vision",
"contractAddress" : "0x103c3A209da59d3E7C4A89307e66521e081CFDF0"
},
{
"decimalCount" : 5,
"symbol" : "GTO",
"name" : "Gifto",
"contractAddress" : "0xc5bbae50781be1669306b9e001eff57a2957b09d"
},
{
"decimalCount" : 8,
"symbol" : "GBX",
"name" : "Globitex",
"contractAddress" : "0x12fcd6463e66974cf7bbc24ffc4d40d6be458283"
},
{
"decimalCount" : 18,
"symbol" : "GNO",
"name" : "Gnosis",
"contractAddress" : "0x6810e776880C02933D47DB1b9fc05908e5386b96"
},
{
"decimalCount" : 18,
"symbol" : "GNT",
"name" : "Golem",
"contractAddress" : "0xa74476443119A942dE498590Fe1f2454d7D4aC0d"
},
{
"decimalCount" : 18,
"symbol" : "HPB",
"name" : "HPBCoin",
"contractAddress" : "0x38c6a68304cdefb9bec48bbfaaba5c5b47818bb2"
},
{
"decimalCount" : 18,
"symbol" : "HYDRO",
"name" : "Hydro",
"contractAddress" : "0xebbdf302c940c6bfd49c6b165f457fdb324649bc"
},
{
"decimalCount" : 18,
"symbol" : "ICX",
"name" : "ICON",
"contractAddress" : "0xb5a5f22694352c15b00323844ad545abb2b11028"
},
{
"decimalCount" : 18,
"symbol" : "ICN",
"name" : "ICONOMI",
"contractAddress" : "0x888666CA69E0f178DED6D75b5726Cee99A87D698"
},
{
"decimalCount" : 9,
"symbol" : "RLC",
"name" : "iExec RLC",
"contractAddress" : "0x607F4C5BB672230e8672085532f7e901544a7375"
},
{
"decimalCount" : 18,
"symbol" : "IOST",
"name" : "IOStoken",
"contractAddress" : "0xfa1a856cfa3409cfa145fa4e20eb270df3eb21ab"
},
{
"decimalCount" : 18,
"symbol" : "ITC",
"name" : "IOT Chain",
"contractAddress" : "0x5e6b6d9abad9093fdc861ea1600eba1b355cd940"
},
{
"decimalCount" : 8,
"symbol" : "IXT",
"name" : "iXledger",
"contractAddress" : "0xfca47962d45adfdfd1ab2d972315db4ce7ccf094"
},
{
"decimalCount" : 18,
"symbol" : "KIN",
"name" : "Kin",
"contractAddress" : "0x818Fc6C2Ec5986bc6E2CBf00939d90556aB12ce5"
},
{
"decimalCount" : 18,
"symbol" : "KNC",
"name" : "Kyber Network",
"contractAddress" : "0xdd974D5C2e2928deA5F71b9825b8b646686BD200"
},
{
"decimalCount" : 18,
"symbol" : "LOOM",
"name" : "Loom",
"contractAddress" : "0xa4e8c3ec456107ea67d3075bf9e3df3a75823db0"
},
{
"decimalCount" : 18,
"symbol" : "LRC",
"name" : "Loopring",
"contractAddress" : "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd"
},
{
"decimalCount" : 18,
"symbol" : "LUN",
"name" : "Lunyr",
"contractAddress" : "0xfa05A73FfE78ef8f1a739473e462c54bae6567D9"
},
{
"decimalCount" : 18,
"symbol" : "MKR",
"name" : "Maker",
"contractAddress" : "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2"
},
{
"decimalCount" : 3,
"symbol" : "GUP",
"name" : "Matchpool",
"contractAddress" : "0xf7B098298f7C69Fc14610bf71d5e02c60792894C"
},
{
"decimalCount" : 18,
"symbol" : "MAN",
"name" : "MATRIX AI Network",
"contractAddress" : "0xe25bcec5d3801ce3a794079bf94adf1b8ccd802d"
},
{
"decimalCount" : 8,
"symbol" : "MTL",
"name" : "Metal",
"contractAddress" : "0xF433089366899D83a9f26A773D59ec7eCF30355e"
},
{
"decimalCount" : 8,
"symbol" : "MCO",
"name" : "Monaco",
"contractAddress" : "0xB63B606Ac810a52cCa15e44bB630fd42D8d1d83d"
},
{
"decimalCount" : 8,
"symbol" : "MYST",
"name" : "Mysterium",
"contractAddress" : "0xa645264C5603E96c3b0B078cdab68733794B0A71"
},
{
"decimalCount" : 18,
"symbol" : "NAS",
"name" : "Nebulas",
"contractAddress" : "0x5d65D971895Edc438f465c17DB6992698a52318D"
},
{
"decimalCount" : 18,
"symbol" : "NEXO",
"name" : "Nexo",
"contractAddress" : "0xb62132e35a6c13ee1ee0f84dc5d40bad8d815206"
},
{
"decimalCount" : 18,
"symbol" : "NCASH",
"name" : "Nucleus Vision",
"contractAddress" : "0x809826cceab68c387726af962713b64cb5cb3cca"
},
{
"decimalCount" : 18,
"symbol" : "NULS",
"name" : "Nuls",
"contractAddress" : "0xb91318f35bdb262e9423bc7c7c2a3a93dd93c92c"
},
{
"decimalCount" : 18,
"symbol" : "NMR",
"name" : "Numeraire",
"contractAddress" : "0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671"
},
{
"decimalCount" : 18,
"symbol" : "OMG",
"name" : "OmiseGO",
"contractAddress" : "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07"
},
{
"decimalCount" : 18,
"symbol" : "OXT",
"name" : "Orchid",
"contractAddress" : "0x4575f41308ec1483f3d399aa9a2826d74da13deb"
},
{
"decimalCount" : 18,
"symbol" : "OST",
"name" : "OST",
"contractAddress" : "0x2C4e8f2D746113d0696cE89B35F0d8bF88E0AEcA"
},
{
"decimalCount" : 18,
"symbol" : "PRL",
"name" : "Oyster Pearl",
"contractAddress" : "0x1844b21593262668b7248d0f57a220caaba46ab9"
},
{
"decimalCount" : 8,
"symbol" : "PTOY",
"name" : "Patientory",
"contractAddress" : "0x8Ae4BF2C33a8e667de34B54938B0ccD03Eb8CC06"
},
{
"decimalCount" : 18,
"symbol" : "PAX",
"name" : "Paxos Standard",
"contractAddress" : "0x8e870d67f660d95d5be530380d0ec0bd388289e1"
},
{
"decimalCount" : 18,
"symbol" : "PMTN",
"name" : "Peer Mountain",
"contractAddress" : "0xe91df2bb9bccd18c45f01843cab88768d64f2d32"
},
{
"decimalCount" : 8,
"symbol" : "POE",
"name" : "Po.et",
"contractAddress" : "0x0e0989b1f9b8a38983c2ba8053269ca62ec9b195"
},
{
"decimalCount" : 18,
"symbol" : "PAL",
"name" : "PolicyPal Network",
"contractAddress" : "0xfeDAE5642668f8636A11987Ff386bfd215F942EE"
},
{
"decimalCount" : 18,
"symbol" : "POLY",
"name" : "Polymath",
"contractAddress" : "0x9992eC3cF6A55b00978cdDF2b27BC6882d88D1eC"
},
{
"decimalCount" : 8,
"symbol" : "PPT",
"name" : "Populous",
"contractAddress" : "0xd4fa1460F537bb9085d22C7bcCB5DD450Ef28e3a"
},
{
"decimalCount" : 6,
"symbol" : "POWR",
"name" : "Power Ledger",
"contractAddress" : "0x595832f8fc6bf59c85c527fec3740a1b7a361269"
},
{
"decimalCount" : 18,
"symbol" : "NPXS",
"name" : "Pundi X Token",
"contractAddress" : "0xa15c7ebe1f07caf6bff097d8a589fb8ac49ae5b3"
},
{
"decimalCount" : 6,
"symbol" : "QASH",
"name" : "QASH",
"contractAddress" : "0x618e75ac90b12c6049ba3b27f5d5f8651b0037f6"
},
{
"decimalCount" : 8,
"symbol" : "QRL",
"name" : "QRL",
"contractAddress" : "0x697beac28B09E122C4332D163985e8a73121b97F"
},
{
"decimalCount" : 18,
"symbol" : "QSP",
"name" : "Quantstamp",
"contractAddress" : "0x99ea4dB9EE77ACD40B119BD1dC4E33e1C070b80d"
},
{
"decimalCount" : 18,
"symbol" : "RDN",
"name" : "Raiden Network",
"contractAddress" : "0x255aa6df07540cb5d3d297f0d0d4d84cb52bc8e6"
},
{
"decimalCount" : 18,
"symbol" : "RCC",
"name" : "Reality Clash",
"contractAddress" : "0x9b6443b0fb9c241a7fdac375595cea13e6b7807a"
},
{
"decimalCount" : 6,
"symbol" : "RC1U03",
"name" : "RentalCoin 1.0",
"contractAddress" : "0x7384db1aaa06364d1e6e67f5434391d3eea2a038"
},
{
"decimalCount" : 18,
"symbol" : "REQ",
"name" : "Request",
"contractAddress" : "0x8f8221aFbB33998d8584A2B05749bA73c37a938a"
},
{
"decimalCount" : 0,
"symbol" : "REV",
"name" : "Revain",
"contractAddress" : "0x48f775efbe4f5ece6e0df2f7b5932df56823b990"
},
{
"decimalCount" : 18,
"symbol" : "RCN",
"name" : "Ripio Credit",
"contractAddress" : "0xf970b8e36e23f7fc3fd752eea86f8be8d83375a6"
},
{
"decimalCount" : 18,
"symbol" : "RVT",
"name" : "Rivetz",
"contractAddress" : "0x3d1ba9be9f66b8ee101911bc36d3fb562eac2244"
},
{
"decimalCount" : 18,
"symbol" : "RUFF",
"name" : "RUFF",
"contractAddress" : "0xf278c1ca969095ffddded020290cf8b5c424ace2"
},
{
"decimalCount" : 18,
"symbol" : "SAI",
"name" : "Sai",
"contractAddress" : "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359"
},
{
"decimalCount" : 8,
"symbol" : "SALT",
"name" : "SALT",
"contractAddress" : "0x4156D3342D5c385a87D264F90653733592000581"
},
{
"decimalCount" : 18,
"symbol" : "SAN",
"name" : "SAN",
"contractAddress" : "0x7C5A0CE9267ED19B22F8cae653F198e3E8daf098"
},
{
"decimalCount" : 8,
"symbol" : "SENT",
"name" : "SENTinel",
"contractAddress" : "0xa44e5137293e855b1b7bc7e2c6f8cd796ffcb037"
},
{
"decimalCount" : 0,
"symbol" : "SNGLS",
"name" : "Singular DTV",
"contractAddress" : "0xaeC2E87E0A235266D9C5ADc9DEb4b2E29b54D009"
},
{
"decimalCount" : 8,
"symbol" : "AGI",
"name" : "SingularityNET",
"contractAddress" : "0x8eB24319393716668D768dCEC29356ae9CfFe285"
},
{
"decimalCount" : 18,
"symbol" : "SRN",
"name" : "SIRIN",
"contractAddress" : "0x68d57c9a1C35f63E2c83eE8e49A64e9d70528D25"
},
{
"decimalCount" : 18,
"symbol" : "SNM",
"name" : "SONM",
"contractAddress" : "0x983F6d60db79ea8cA4eB9968C6aFf8cfA04B3c63"
},
{
"decimalCount" : 18,
"symbol" : "SNT",
"name" : "Status",
"contractAddress" : "0x744d70FDBE2Ba4CF95131626614a1763DF805B9E"
},
{
"decimalCount" : 18,
"symbol" : "STK",
"name" : "STK",
"contractAddress" : "0xae73b38d1c9a8b274127ec30160a4927c4d71824"
},
{
"decimalCount" : 18,
"symbol" : "STQ",
"name" : "Storiqa",
"contractAddress" : "0x5c3a228510d246b78a3765c20221cbf3082b44a4"
},
{
"decimalCount" : 8,
"symbol" : "STORJ",
"name" : "Storj",
"contractAddress" : "0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC"
},
{
"decimalCount" : 18,
"symbol" : "STORM",
"name" : "Storm",
"contractAddress" : "0xD0a4b8946Cb52f0661273bfbC6fD0E0C75Fc6433"
},
{
"decimalCount" : 18,
"symbol" : "STMX",
"name" : "StormX",
"contractAddress" : "0xbE9375C6a420D2eEB258962efB95551A5b722803"
},
{
"decimalCount" : 18,
"symbol" : "STX",
"name" : "Stox",
"contractAddress" : "0x006BeA43Baa3f7A6f765F14f10A1a1b08334EF45"
},
{
"decimalCount" : 18,
"symbol" : "SUB",
"name" : "Substratum",
"contractAddress" : "0x8D75959f1E61EC2571aa72798237101F084DE63a"
},
{
"decimalCount" : 18,
"symbol" : "SNC",
"name" : "SunContract",
"contractAddress" : "0xF4134146AF2d511Dd5EA8cDB1C4AC88C57D60404"
},
{
"decimalCount" : 18,
"symbol" : "SUSHI",
"name" : "SushiSwap",
"contractAddress" : "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2"
},
{
"decimalCount" : 2,
"symbol" : "TEL",
"name" : "Telcoin",
"contractAddress" : "0x467bccd9d29f223bce8043b84e8c8b282827790f"
},
{
"decimalCount" : 18,
"symbol" : "PAY",
"name" : "TenX",
"contractAddress" : "0xB97048628DB6B661D4C2aA833e95Dbe1A905B280"
},
{
"decimalCount" : 6,
"symbol" : "USDT",
"name" : "Tether USD",
"contractAddress" : "0xdac17f958d2ee523a2206206994597c13d831ec7"
},
{
"decimalCount" : 18,
"symbol" : "THETA",
"name" : "Theta Token",
"contractAddress" : "0x3883f5e181fccaf8410fa61e12b59bad963fb645"
},
{
"decimalCount" : 6,
"symbol" : "TRX",
"name" : "TRON",
"contractAddress" : "0xf230b790e05390fc8295f4d3f60332c93bed42e2"
},
{
"decimalCount" : 18,
"symbol" : "TUSD",
"name" : "TrueUSD",
"contractAddress" : "0x0000000000085d4780B73119b644AE5ecd22b376"
},
{
"decimalCount" : 8,
"symbol" : "UCASH",
"name" : "UCASH",
"contractAddress" : "0x92e52a1a235d9a103d970901066ce910aacefd37"
},
{
"decimalCount" : 18,
"symbol" : "UNI",
"name" : "Uniswap",
"contractAddress" : "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"
},
{
"decimalCount" : 6,
"symbol" : "USDC",
"name" : "USD Coin",
"contractAddress" : "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
},
{
"decimalCount" : 18,
"symbol" : "VIB",
"name" : "Viberate",
"contractAddress" : "0x2C974B2d0BA1716E644c1FC59982a89DDD2fF724"
},
{
"decimalCount" : 8,
"symbol" : "VGX",
"name" : "Voyager",
"contractAddress" : "0x5af2be193a6abca9c8817001f45744777db30756"
},
{
"decimalCount" : 18,
"symbol" : "WTC",
"name" : "Waltonchain",
"contractAddress" : "0xb7cb1c96db6b22b0d3d9536e0108d062bd488f74"
},
{
"decimalCount" : 8,
"symbol" : "WAX",
"name" : "WAX",
"contractAddress" : "0x39Bb259F66E1C59d5ABEF88375979b4D20D98022"
},
{
"decimalCount" : 18,
"symbol" : "WPR",
"name" : "WePower",
"contractAddress" : "0x4CF488387F035FF08c371515562CBa712f9015d4"
},
{
"decimalCount" : 18,
"symbol" : "WINGS",
"name" : "Wings",
"contractAddress" : "0x667088b212ce3d06a1b553a7221E1fD19000d9aF"
},
{
"decimalCount" : 6,
"symbol" : "WRC",
"name" : "Worldcore",
"contractAddress" : "0x72adadb447784dd7ab1f472467750fc485e4cb2d"
},
{
"decimalCount" : 18,
"symbol" : "ZLA",
"name" : "Zilla",
"contractAddress" : "0xfd8971d5e8e1740ce2d0a84095fca4de729d0c16"
},
{
"decimalCount" : 0,
"symbol" : "GD",
"name" : "Zilla GD",
"contractAddress" : "0xb4cebdb66b3a5183fd4764a25cad1fc109535016"
},
{
"decimalCount" : 12,
"symbol" : "ZIL",
"name" : "Zilliqa",
"contractAddress" : "0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27"
}
]

@ -0,0 +1 @@
Subproject commit 9a731fcb5a58d53c90d7db79e5441bdf18e087fc

View file

@ -6,7 +6,9 @@ import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.snackbar.Snackbar
import com.tangem.CardFilter
import com.tangem.Config
import com.tangem.TangemSdk
@ -52,9 +54,13 @@ private fun initCoroutineExceptionHandler(): CoroutineExceptionHandler {
}
class MainActivity : AppCompatActivity() {
private var snackbar: Snackbar? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
store.state.globalState.feedbackManager?.updateAcivity(this)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
@ -103,4 +109,21 @@ class MainActivity : AppCompatActivity() {
store.dispatch(NavigationAction.ActivityDestroyed)
super.onDestroy()
}
fun showSnackbar(text: Int, buttonTitle: Int? = null, action: View.OnClickListener? = null) {
if (snackbar != null) return
snackbar = Snackbar.make(
fragment_container, getString(text), Snackbar.LENGTH_INDEFINITE
)
if (buttonTitle != null && action != null) {
snackbar?.setAction(getString(buttonTitle), action)
}
snackbar?.show()
}
fun dismissSnackbar() {
snackbar?.dismiss()
snackbar = null
}
}

View file

@ -69,12 +69,11 @@ class TapApplication : Application() {
private fun initFeedbackManager() {
val infoHolder = AdditionalEmailInfo()
infoHolder.updateAppVersion(this)
infoHolder.setAppVersion(this)
val logWriter = TangemLogCollector()
Log.addLogger(logWriter)
val feedbackManager = FeedbackManager(infoHolder, this, logWriter)
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
store.dispatch(GlobalAction.SetFeedbackManager(FeedbackManager(infoHolder, logWriter)))
}
}

View file

@ -7,15 +7,15 @@ import com.tangem.wallet.R
@DrawableRes
fun Blockchain.getIconRes(): Int {
return when (this) {
Blockchain.Unknown, Blockchain.Ducatus, Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet,
Blockchain.BinanceTestnet -> 1
Blockchain.Bitcoin -> R.drawable.ic_btc
Blockchain.Unknown -> R.drawable.shape_circle
Blockchain.Ducatus -> R.drawable.ic_ducatus
Blockchain.Bitcoin, Blockchain.BitcoinTestnet,-> R.drawable.ic_btc
Blockchain.BitcoinCash -> R.drawable.ic_btc_cash
Blockchain.Litecoin -> R.drawable.ic_ltc
Blockchain.Ethereum -> R.drawable.ic_eth
Blockchain.Ethereum, Blockchain.EthereumTestnet, -> R.drawable.ic_eth
Blockchain.RSK -> R.drawable.ic_rsk
Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano
Blockchain.Binance -> R.drawable.ic_binance
Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_binance
Blockchain.Tezos -> R.drawable.ic_tezos
Blockchain.XRP -> R.drawable.ic_xrp
Blockchain.Stellar -> R.drawable.ic_stellar

View file

@ -0,0 +1,11 @@
package com.tangem.tap.common.extensions
/**
[REDACTED_AUTHOR]
*/
fun <T> List<T>.containsAny(list: List<T>): Boolean {
this.forEach { mainItem ->
list.forEach { if (it == mainItem) return true }
}
return false
}

View file

@ -3,9 +3,14 @@ package com.tangem.tap.common.extensions
import androidx.annotation.ColorInt
import androidx.core.graphics.toColorInt
import com.tangem.blockchain.common.Token
import com.tangem.wallet.R
@ColorInt
fun Token.getColor(): Int {
return ("#" + this.contractAddress.subSequence(2..7).toString())
.toColorInt()
return try {
("#" + this.contractAddress.subSequence(2..7).toString())
.toColorInt()
} catch (exception: Exception) {
R.color.lightGray4
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux.global
import com.tangem.common.CompletionResult
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
@ -11,18 +12,25 @@ import org.rekotlin.Action
sealed class GlobalAction : Action {
object ScanFailsCounter {
data class ChooseBehavior(val result: CompletionResult<ScanNoteResponse>) : GlobalAction()
object Reset : GlobalAction()
object Increment : GlobalAction()
}
data class SaveScanNoteResponse(val scanNoteResponse: ScanNoteResponse) : GlobalAction()
data class ChangeAppCurrency(val appCurrency: FiatCurrencyName) : GlobalAction()
object RestoreAppCurrency : GlobalAction() {
data class Success(val appCurrency: FiatCurrencyName) : GlobalAction()
}
data class UpdateWalletSignedHashes(val walletSignedHashes: Int?) : GlobalAction()
data class HideWarningMessage(val warning: WarningMessage) : GlobalAction()
data class UpdateSecurityOptions(val securityOption: SecurityOption) : GlobalAction()
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: FeedbackManager): GlobalAction()
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
data class SendFeedback(val emailData: EmailData): GlobalAction()
data class SendFeedback(val emailData: EmailData) : GlobalAction()
}

View file

@ -1,8 +1,10 @@
package com.tangem.tap.common.redux.global
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.preferencesStorage
@ -13,6 +15,22 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
when (action) {
is GlobalAction.ScanFailsCounter.ChooseBehavior -> {
when (action.result) {
is CompletionResult.Success -> store.dispatch(GlobalAction.ScanFailsCounter.Reset)
is CompletionResult.Failure -> {
if (action.result.error is TangemSdkError.UserCancelled) {
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
if (store.state.globalState.scanCardFailsCounter >= 2) {
store.dispatch(HomeAction.ShowDialog.ScanFails)
store.dispatch(WalletAction.ShowDialog.ScanFails)
}
} else {
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
}
}
}
}
is GlobalAction.RestoreAppCurrency -> {
store.dispatch(GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.getAppCurrency()
@ -26,10 +44,8 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
}
store.dispatch(WalletAction.Warnings.SetWarnings(
it.getWarnings(WarningMessage.Location.MainScreen)))
store.dispatch(SendAction.SetWarnings(
it.getWarnings(WarningMessage.Location.SendScreen)))
store.dispatch(WalletAction.Warnings.Update)
store.dispatch(SendAction.Warnings.Update)
}
}
}

View file

@ -11,6 +11,12 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
val globalState = state.globalState
return when (action) {
is GlobalAction.ScanFailsCounter.Increment -> {
globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1)
}
is GlobalAction.ScanFailsCounter.Reset -> {
globalState.copy(scanCardFailsCounter = 0)
}
is GlobalAction.SaveScanNoteResponse ->
globalState.copy(scanNoteResponse = action.scanNoteResponse)
is GlobalAction.ChangeAppCurrency -> {
@ -33,7 +39,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState.copy(configManager = action.configManager)
}
is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager)
is GlobalAction.HideWarningMessage -> globalState
is GlobalAction.UpdateSecurityOptions -> {
val card = when (action.securityOption) {
SecurityOption.LongTap -> globalState.scanNoteResponse?.card?.copy(

View file

@ -20,9 +20,12 @@ data class GlobalState(
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val scanCardFailsCounter: Int = 0,
) : StateType
typealias CryptoCurrencyName = String
typealias FiatCurrencyName = String
interface StateDialog

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.*
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.common.network.Result
import com.tangem.tap.common.analytics.AnalyticsEvent
@ -13,6 +14,7 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.extensions.isNoAccountError
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.features.tokens.redux.TokensAction
@ -31,7 +33,7 @@ class TapWalletManager {
private val blockchainSdkConfig by lazy {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private val walletManagerFactory: WalletManagerFactory
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
suspend fun loadWalletData(walletManager: WalletManager) {
@ -84,9 +86,8 @@ class TapWalletManager {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.CARD_IS_SCANNED, data.card)
}
TapWorkarounds.updateCard(data.card)
store.state.globalState.warningManager?.setBlockchain(data.walletManager?.wallet?.blockchain)
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data.card)
updateConfigManager(data)
updateFeedbackManager(data)
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.ResetState)
@ -124,61 +125,27 @@ class TapWalletManager {
}
}
private fun updateFeedbackManager(data: ScanNoteResponse) {
val card = data.card
val wallet = data.walletManager?.wallet ?: return
val infoHolder = store.state.globalState.feedbackManager?.infoHolder ?: return
infoHolder.cardId = card.cardId
infoHolder.cardFirmwareVersion = card.firmwareVersion.version
infoHolder.signedHashesCount = card.walletSignedHashes?.toString() ?: "0"
infoHolder.sourceAddress = wallet.address
infoHolder.explorerLink = wallet.getExploreUrl(wallet.address)
infoHolder.blockchain = wallet.blockchain
}
suspend fun loadData(data: ScanNoteResponse) {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.Warnings.CheckIfNeeded)
val artworkId = data.verifyResponse?.artworkInfo?.id
if (data.walletManager != null) {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection))
return@withContext
}
val config = store.state.globalState.configManager?.config ?: return@withContext
val primaryWalletManager = data.walletManager
val primaryBlockchain = listOf(data.walletManager.wallet.blockchain)
val primaryBlockchain = data.walletManager.wallet.blockchain
val primaryTokenSymbol = data.card.cardData?.tokenSymbol
val primaryToken = primaryWalletManager.presetTokens.toList()
.firstOrNull { it.symbol == primaryTokenSymbol }
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(primaryBlockchain[0]))
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(primaryBlockchain))
if (primaryToken != null) {
store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken))
}
if (data.card.isMultiwalletAllowed) {
val savedCurrencies = currenciesRepository.loadCardCurrencies(data.card.cardId)
val walletManagers = listOf(primaryWalletManager) +
currenciesRepository.getBlockchains()
.filterNot { it == primaryWalletManager.wallet.blockchain }
.mapNotNull { walletManagerFactory.makeWalletManager(data.card, it) }
val otherBlockhains = savedCurrencies.blockchains
val activeTokens = walletManagers.first { it.wallet.blockchain == Blockchain.Ethereum}
.presetTokens.toList()
val tokens = activeTokens + savedCurrencies.tokens
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(primaryBlockchain + otherBlockhains))
store.dispatch(WalletAction.MultiWallet.AddTokens(tokens))
store.dispatch(WalletAction.MultiWallet.FindTokensInUse)
loadMultiWalletData(data.card, primaryBlockchain, primaryWalletManager)
} else {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(listOf(primaryWalletManager)))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(primaryBlockchain))
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain)))
}
store.dispatch(WalletAction.SetArtworkId(data.verifyResponse?.artworkInfo?.id))
store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled))
@ -191,6 +158,35 @@ class TapWalletManager {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
}
store.dispatch(WalletAction.Warnings.CheckIfNeeded)
}
}
private fun loadMultiWalletData(
card: Card, primaryBlockchain: Blockchain, primaryWalletManager: WalletManager
) {
val presetTokens = primaryWalletManager.presetTokens.toList()
val savedCurrencies = currenciesRepository.loadCardCurrencies(card.cardId)
if (savedCurrencies == null) {
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
CardCurrencies(
blockchains = listOf(primaryBlockchain), tokens = presetTokens
)))
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain)))
store.dispatch(WalletAction.MultiWallet.AddTokens(presetTokens))
store.dispatch(WalletAction.MultiWallet.FindBlockchainsInUse(card, walletManagerFactory))
store.dispatch(WalletAction.MultiWallet.FindTokensInUse)
} else {
val blockchains = listOf(primaryBlockchain) + savedCurrencies.blockchains
val walletManagers = walletManagerFactory.makeWalletManagers(card, blockchains)
val tokens = presetTokens + savedCurrencies.tokens
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains))
store.dispatch(WalletAction.MultiWallet.AddTokens(tokens))
store.dispatch(WalletAction.MultiWallet.FindTokensInUse)
}
}
@ -217,6 +213,10 @@ class TapWalletManager {
when (result) {
is Result.Success -> store.dispatch(WalletAction.LoadWallet.Success(result.data))
is Result.Failure -> {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection))
return@withContext
}
val error = result.error
val blockchain = walletManager.wallet.blockchain
if (error != null && blockchain.isNoAccountError(error)) {

View file

@ -10,7 +10,7 @@ interface Loader<T> {
companion object {
const val featuresName = "features_${BuildConfig.CONFIG_ENVIRONMENT}"
const val configValuesName = "config_${BuildConfig.CONFIG_ENVIRONMENT}"
const val configValuesName = "tangem-android-config/config_${BuildConfig.CONFIG_ENVIRONMENT}"
const val warnings = "warnings_${BuildConfig.CONFIG_ENVIRONMENT}"
}
}

View file

@ -31,11 +31,12 @@ class ConfigManager(
localLoader.load { config ->
setupFeature(config.features)
setupKey(config.configValues)
}
remoteLoader.load { config ->
setupFeature(config.features)
onComplete?.invoke()
}
// Uncomment to enable remote config
// remoteLoader.load { config ->
// setupFeature(config.features)
// }
}
fun turnOff(name: String) {
@ -79,7 +80,7 @@ class ConfigManager(
blockchainSdkConfig = BlockchainSdkConfig(
blockchairApiKey = values.blockchairApiKey,
blockcypherTokens = values.blockcypherTokens,
infuraProjectId = values.infuraProjectId
infuraProjectId = values.infuraProjectId
)
)
defaultConfig = defaultConfig.copy(
@ -89,7 +90,7 @@ class ConfigManager(
blockchainSdkConfig = BlockchainSdkConfig(
blockchairApiKey = values.blockchairApiKey,
blockcypherTokens = values.blockcypherTokens,
infuraProjectId = values.infuraProjectId
infuraProjectId = values.infuraProjectId
)
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.domain.configurable.warningMessage
import com.tangem.blockchain.common.Blockchain
import com.tangem.tangem_sdk_new.ui.animation.VoidCallback
import com.tangem.tap.common.extensions.containsAny
import com.tangem.wallet.R
/**
@ -11,7 +12,6 @@ class WarningMessagesManager(
private val warningLoader: RemoteWarningLoader,
) {
private var blockchain: Blockchain? = null
private val warningsList: MutableList<WarningMessage> = mutableListOf()
fun load(onComplete: VoidCallback? = null) {
@ -23,10 +23,6 @@ class WarningMessagesManager(
}
}
fun setBlockchain(blockchain: Blockchain?) {
this.blockchain = blockchain
}
fun addWarning(warning: WarningMessage) {
if (findWarning(warning) == null) {
warningsList.add(warning)
@ -34,14 +30,14 @@ class WarningMessagesManager(
}
}
fun getWarnings(location: WarningMessage.Location): List<WarningMessage> {
fun getWarnings(location: WarningMessage.Location, forBlockchains: List<Blockchain> = emptyList()): List<WarningMessage> {
return warningsList
.filter { !it.isHidden && it.location.contains(location) }
.filter {
val blockchainList = it.blockchainList
val list = it.blockchainList
when {
blockchainList == null -> true
blockchainList.contains(blockchain) -> true
list == null -> true
list.containsAny(forBlockchains) -> true
else -> false
}
}
@ -117,5 +113,17 @@ class WarningMessagesManager(
fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean {
return warning.messageResId == R.string.alert_card_signed_transactions
}
fun onlineVerificationFailed(): WarningMessage = WarningMessage(
"",
"",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_failed_to_verify_card_title,
R.string.warning_failed_to_verify_card_message,
WarningMessage.Origin.Local
)
}
}

View file

@ -1,9 +1,6 @@
package com.tangem.tap.domain.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.TangemError
import com.tangem.TangemSdkError
import com.tangem.*
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
@ -32,10 +29,6 @@ data class ScanNoteResponse(
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteResponse> {
override val requiresPin2 = false
private val blockchainSdkConfig = store.state.globalState.configManager?.config
?.blockchainSdkConfig ?: BlockchainSdkConfig()
private val walletManagerFactory = WalletManagerFactory(blockchainSdkConfig)
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
ScanTask().run(session) { result ->
when (result) {
@ -59,7 +52,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
}
val walletManager = try {
walletManagerFactory.makeWalletManager(card)
getWalletManagerFactory().makeWalletManager(card)
} catch (exception: Exception) {
return@run callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
@ -100,7 +93,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletManager = try {
walletManagerFactory.makeMultisigWalletManager(card, twinPublicKey)
getWalletManagerFactory().makeMultisigWalletManager(card, twinPublicKey)
} catch (exception: Exception) {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
return@run
@ -124,5 +117,10 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return null
}
private fun getWalletManagerFactory(): WalletManagerFactory {
val blockchainSdkConfig = store.state.globalState.configManager?.config
?.blockchainSdkConfig ?: BlockchainSdkConfig()
return WalletManagerFactory(blockchainSdkConfig)
}
}

View file

@ -19,8 +19,11 @@ class CurrenciesRepository(val context: Application) {
Types.newParameterizedType(List::class.java, Blockchain::class.java)
)
fun loadCardCurrencies(cardId: String): CardCurrencies {
return CardCurrencies(loadSavedTokens(cardId), loadSavedBlockchains(cardId))
fun loadCardCurrencies(cardId: String): CardCurrencies? {
val blockchains = loadSavedBlockchains(cardId)
if (blockchains.isEmpty()) return null
return CardCurrencies(loadSavedTokens(cardId), blockchains)
}
fun saveCardCurrencies(cardId: String, currencies: CardCurrencies) {

View file

@ -1,20 +1,30 @@
package com.tangem.tap.features.feedback
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.tangem.Log
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.store
import timber.log.Timber
import java.io.File
import java.io.FileWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.*
/**
@ -22,39 +32,60 @@ import java.io.StringWriter
*/
class FeedbackManager(
val infoHolder: AdditionalEmailInfo,
private val context: Context,
private val logCollector: TangemLogCollector,
private val email: String = "support@tangem.com",
) {
private lateinit var activity: Activity
fun updateAcivity(activity: Activity) {
this.activity = activity
}
fun send(emailData: EmailData) {
val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null
sendTo(email, emailData.subject, emailData.joinTogether(infoHolder), fileLog)
}
private fun sendTo(email: String, subject: String, message: String, fileLog: File? = null) {
val intent = Intent(Intent.ACTION_SENDTO).apply {
data = Uri.parse("mailto:$email")
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, message)
fileLog?.let {
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", it)
putExtra(Intent.EXTRA_STREAM, uri)
}
}
val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
val originalIntentResults = activity.packageManager.queryIntentActivities(emailFilterIntent, 0)
val emailFilterIntentResults = activity.packageManager.queryIntentActivities(emailFilterIntent, 0)
val targetedIntents = originalIntentResults
.filter { originalResult ->
emailFilterIntentResults.any {
originalResult.activityInfo.packageName == it.activityInfo.packageName
}
}
.map {
createEmailShareIntent(email, subject, message, fileLog).apply {
setPackage(it.activityInfo.packageName)
}
}
.toMutableList()
try {
val chooserIntent = Intent.createChooser(intent, "Send mail...")
val chooserIntent = Intent.createChooser(targetedIntents.removeAt(0), "Send mail...")
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray())
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ContextCompat.startActivity(context, chooserIntent, null)
ContextCompat.startActivity(activity, chooserIntent, null)
} catch (ex: ActivityNotFoundException) {
Timber.e(ex)
}
}
private fun createEmailShareIntent(recipient: String, subject: String, text: String, file: File? = null): Intent {
val builder = ShareCompat.IntentBuilder.from(activity)
.setType("message/rfc822")
.setEmailTo(arrayOf(recipient))
.setSubject(subject)
.setText(text)
file?.let { builder.setStream(FileProvider.getUriForFile(activity, "${activity.packageName}.provider", it)) }
return builder.intent
}
private fun createLogFile(): File? {
return try {
val file = File(context.filesDir, "logs.txt")
val file = File(activity.filesDir, "logs.txt")
file.delete()
file.createNewFile()
@ -73,10 +104,12 @@ class FeedbackManager(
}
class TangemLogCollector : TangemSdkLogger {
private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS")
private val logs = mutableListOf<String>()
override fun log(message: () -> String, level: Log.Level) {
logs.add(message())
val time = dateFormatter.format(Date())
logs.add("$time: ${message()}\n")
}
fun getLogs(): List<String> = logs.toList()
@ -87,26 +120,36 @@ class TangemLogCollector : TangemSdkLogger {
}
class AdditionalEmailInfo {
var cardId: String = ""
var cardFirmwareVersion: String = ""
var blockchain: Blockchain = Blockchain.Unknown
class EmailWalletInfo(
var blockchain: Blockchain = Blockchain.Unknown,
var address: String = "",
var explorerLink: String = "",
// var outputsCount: String = ""
// var transactionHex: String = ""
)
var phoneModel: String = Build.MODEL
var osVersion: String = Build.VERSION.SDK_INT.toString()
var appVersion: String = ""
var token: String = ""
var sourceAddress: String = ""
// card
var cardId: String = ""
var cardFirmwareVersion: String = ""
// wallets
internal val walletsInfo = mutableListOf<EmailWalletInfo>()
internal var onSendErrorWalletInfo: EmailWalletInfo? = null
var signedHashesCount: String = ""
// device
var phoneModel: String = Build.MODEL
var osVersion: String = Build.VERSION.SDK_INT.toString()
// send error
var destinationAddress: String = ""
var amount: String = ""
var fee: String = ""
var token: String = ""
// var transactionHex: String = ""
var signedHashesCount: String = ""
var explorerLink: String = ""
// var outputsCount: String = ""
fun updateAppVersion(context: Context) {
fun setAppVersion(context: Context) {
try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
appVersion = pInfo.versionName
@ -114,6 +157,51 @@ class AdditionalEmailInfo {
e.printStackTrace()
}
}
fun setCardInfo(card: Card) {
cardId = card.cardId
cardFirmwareVersion = card.firmwareVersion.version
signedHashesCount = card.walletSignedHashes?.toString() ?: "0"
}
fun setWalletsInfo(wallets: List<Wallet>) {
walletsInfo.clear()
wallets.forEach { walletsInfo.add(EmailWalletInfo(it.blockchain, getAddress(it), getExploreUri(it))) }
}
fun updateOnSendError(wallet: Wallet, amountToSend: Amount, feeAmount: Amount, destinationAddress: String) {
val amountState = store.state.sendState.amountState
onSendErrorWalletInfo = EmailWalletInfo(wallet.blockchain, getAddress(wallet), getExploreUri(wallet))
this.destinationAddress = destinationAddress
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
if (amountState.typeOfAmount is AmountType.Token) {
token = amountState.amountToExtract?.currencySymbol ?: ""
}
}
private fun getAddress(wallet: Wallet): String {
return if (wallet.addresses.size == 1) {
wallet.address
} else {
val addresses = wallet.addresses.joinToString(", ") {
"${it.type.javaClass.simpleName} - ${it.value}"
}
"Multiple address: $addresses"
}
}
private fun getExploreUri(wallet: Wallet): String {
return if (wallet.addresses.size == 1) {
wallet.getExploreUrl(wallet.address)
} else {
val links = wallet.addresses.joinToString(", ") {
"${it.type.javaClass.simpleName} - ${wallet.getExploreUrl(it.value)}"
}
"Multiple explorers links: $links"
}
}
}
interface EmailData {
@ -133,9 +221,10 @@ class RateCanBeBetterEmail : EmailData {
override val mainMessage: String = "Tell us what functions you are missing, and we will try to help you."
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
val walletInfo = infoHolder.walletsInfo[0]
return StringBuilder().apply {
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Blockchain", infoHolder.blockchain.fullName)
appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
@ -159,12 +248,13 @@ class SendTransactionFailedEmail(private val error: String) : EmailData {
override val subject: String = "Cant send a transaction"
override val mainMessage: String = "Please tell us more about your issue. Every small detail can help."
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo()
return StringBuilder().apply {
appendKeyValue("Error", error)
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Blockchain", infoHolder.blockchain.fullName)
appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
appendKeyValue("Token", infoHolder.token)
appendKeyValue("Source address", infoHolder.sourceAddress)
appendKeyValue("Source address", walletInfo.address)
appendKeyValue("Destination address", infoHolder.destinationAddress)
appendKeyValue("Amount", infoHolder.amount)
appendKeyValue("Fee", infoHolder.fee)
@ -181,17 +271,20 @@ class FeedbackEmail : EmailData {
override val subject: String = "Tangem Tap feedback"
override val mainMessage: String = "Hi Tangem,"
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
appendKeyValue("Blockchain", infoHolder.blockchain.fullName)
appendKeyValue("Wallet address", infoHolder.sourceAddress)
appendKeyValue("Explorer link", infoHolder.explorerLink)
val builder = StringBuilder()
builder.appendKeyValue("Card ID", infoHolder.cardId)
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
infoHolder.walletsInfo.forEach {
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
builder.appendKeyValue("Wallet address", it.address)
builder.appendKeyValue("Explorer link", it.explorerLink)
}
// appendKeyValue("Outputs count", infoHolder.outputsCount)
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
}.toString()
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
builder.appendKeyValue("OS version", infoHolder.osVersion)
return builder.toString()
}
}

View file

@ -1,11 +1,15 @@
package com.tangem.tap.features.home
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeDialog
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_home.*
@ -13,6 +17,8 @@ import org.rekotlin.StoreSubscriber
class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState> {
private var dialog: Dialog? = null
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
@ -39,6 +45,19 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
btn_shop.text = getText(R.string.home_button_shop)
btn_yes.text = getText(R.string.home_button_scan)
}
handleDialog(state.dialog)
}
private fun handleDialog(stateDialog: StateDialog?) {
when (stateDialog) {
is HomeDialog.ScanFailsDialog -> {
if (dialog == null) dialog = ScanFailsDialog.create(requireContext()).apply { show() }
}
else -> {
dialog?.dismiss()
dialog = null
}
}
}

View file

@ -9,4 +9,10 @@ sealed class HomeAction : Action {
object CheckIfFirstLaunch : HomeAction() {
data class Result(val firstLaunch: Boolean) : HomeAction()
}
object ShowDialog : HomeAction() {
object ScanFails : HomeAction()
}
object HideDialog : HomeAction()
}

View file

@ -32,6 +32,7 @@ class HomeMiddleware {
scope.launch {
val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler)
withContext(Dispatchers.Main) {
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)

View file

@ -18,6 +18,12 @@ private fun internalReduce(action: Action, state: AppState): HomeState {
is HomeAction.CheckIfFirstLaunch.Result -> {
homeState = homeState.copy(firstLaunch = action.firstLaunch)
}
is HomeAction.ShowDialog.ScanFails -> {
homeState = homeState.copy(dialog = HomeDialog.ScanFailsDialog)
}
is HomeAction.HideDialog -> {
homeState = homeState.copy(dialog = null)
}
}
return homeState

View file

@ -1,7 +1,13 @@
package com.tangem.tap.features.home.redux
import com.tangem.tap.common.redux.global.StateDialog
import org.rekotlin.StateType
data class HomeState(
val firstLaunch: Boolean = true
val firstLaunch: Boolean = true,
val dialog: StateDialog? = null
) : StateType
sealed class HomeDialog: StateDialog {
object ScanFailsDialog: HomeDialog()
}

View file

@ -3,8 +3,10 @@ package com.tangem.tap.features.send.redux
import com.tangem.Message
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.features.send.redux.states.FeeType
@ -25,6 +27,7 @@ object ReleaseSendState : Action
data class PrepareSendScreen(
val coinAmount: Amount?,
val coinRate: BigDecimal?,
val walletManager: WalletManager?,
val tokenAmount: Amount? = null,
val tokenRate: BigDecimal? = null
) : SendScreenAction
@ -140,14 +143,19 @@ sealed class SendAction : SendScreenAction {
data class SendError(override val error: TapError) : SendAction(), ErrorAction
sealed class Dialog : SendAction() {
sealed class Dialog : SendAction(), StateDialog {
data class TezosWarningDialog(
val reduceCallback: () -> Unit,
val sendAllCallback: () -> Unit,
val reduceAmount: BigDecimal,
) : Dialog()
data class SendTransactionFails(val errorMessage: String): Dialog()
data class SendTransactionFails(val errorMessage: String) : Dialog()
object Hide : Dialog()
}
data class SetWarnings(val warningList: List<WarningMessage>) : SendAction()
sealed class Warnings : SendAction() {
object Update : SendAction()
data class Set(val warningList: List<WarningMessage>) : SendAction()
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.TapWorkarounds
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.minimalAmount
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
@ -46,6 +47,7 @@ val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
is SendActionUi.SendAmountToRecipient ->
verifyAndSendTransaction(action, appState(), dispatch)
is PrepareSendScreen -> setIfSendingToPayIdEnabled(appState(), dispatch)
is SendAction.Warnings.Update -> updateWarnings(dispatch)
}
nextDispatch(action)
}
@ -56,8 +58,8 @@ private fun verifyAndSendTransaction(
action: SendActionUi.SendAmountToRecipient, appState: AppState?, dispatch: (Action) -> Unit,
) {
val sendState = appState?.sendState ?: return
val walletManager = appState.globalState.scanNoteResponse?.walletManager ?: return
val card = appState.globalState.scanNoteResponse.card
val walletManager = sendState.walletManager ?: return
val card = appState.globalState.scanNoteResponse?.card ?: return
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress ?: return
val typedAmount = sendState.amountState.amountToExtract ?: return
val feeAmount = sendState.feeState.currentFee ?: return
@ -120,6 +122,9 @@ private fun sendTransaction(
dispatch(GlobalAction.UpdateWalletSignedHashes(result.data.walletSignedHashes))
dispatch(NavigationAction.PopBackTo())
scope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
dispatch(WalletAction.UpdateWallet(walletManager.wallet.blockchain.currency))
}
delay(10000)
withContext(Dispatchers.Main) {
dispatch(WalletAction.UpdateWallet(walletManager.wallet.blockchain.currency))
@ -142,10 +147,11 @@ private fun sendTransaction(
is Throwable -> {
val throwable = result.error as Throwable
val message = throwable.message
val infoHolder = store.state.globalState.feedbackManager?.infoHolder
when {
message == null -> {
dispatch(SendAction.SendError(TapError.UnknownError))
updateFeedbackManager(walletManager, amountToSend, feeAmount, destinationAddress, card)
infoHolder?.updateOnSendError(walletManager.wallet, amountToSend, feeAmount, destinationAddress)
dispatch(SendAction.Dialog.SendTransactionFails("unknown error"))
}
message.contains("50002") -> {
@ -160,7 +166,7 @@ private fun sendTransaction(
Timber.e(throwable)
FirebaseCrashlytics.getInstance().recordException(throwable)
dispatch(SendAction.SendError(TapError.CustomError(message)))
updateFeedbackManager(walletManager, amountToSend, feeAmount, destinationAddress, card)
infoHolder?.updateOnSendError(walletManager.wallet, amountToSend, feeAmount, destinationAddress)
dispatch(SendAction.Dialog.SendTransactionFails(message))
}
}
@ -173,29 +179,6 @@ private fun sendTransaction(
}
}
private fun updateFeedbackManager(
walletManager: WalletManager,
amountToSend: Amount,
feeAmount: Amount,
destinationAddress: String,
card: Card,
) {
val infoHolder = store.state.globalState.feedbackManager?.infoHolder ?: return
val amountState = store.state.sendState.amountState
infoHolder.cardId = card.cardId
infoHolder.blockchain = walletManager.wallet.blockchain
infoHolder.sourceAddress = walletManager.wallet.address
infoHolder.destinationAddress = destinationAddress
infoHolder.amount = amountToSend.value?.stripZeroPlainString() ?: "0"
infoHolder.fee = feeAmount.value?.stripZeroPlainString() ?: "0"
infoHolder.cardFirmwareVersion = card.firmwareVersion.version
if (amountState.typeOfAmount is AmountType.Token) {
infoHolder.token = amountState.amountToExtract?.currencySymbol ?: ""
}
// infoHolder.transactionHex = ""
}
fun extractErrorsForAmountField(errors: EnumSet<TransactionError>): EnumSet<TransactionError> {
val showIntoAmountField = EnumSet.noneOf(TransactionError::class.java)
errors.forEach {
@ -243,3 +226,11 @@ private fun setIfSendingToPayIdEnabled(appState: AppState?, dispatch: (Action) -
dispatch(AddressPayIdActionUi.ChangePayIdState(isSendingToPayIdEnabled))
}
private fun updateWarnings(dispatch: (Action) -> Unit) {
val warningsManager = store.state.globalState.warningManager ?: return
val blockchain = store.state.sendState.walletManager?.wallet?.blockchain ?: return
val warnings = warningsManager.getWarnings(WarningMessage.Location.SendScreen, listOf(blockchain))
dispatch(SendAction.Warnings.Set(warnings))
}

View file

@ -45,7 +45,7 @@ private class SendReducer : SendInternalReducer {
is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails -> sendState.copy(dialog = action)
is SendAction.Dialog.Hide -> sendState.copy(dialog = null)
is SendAction.SetWarnings -> sendState.copy(sendWarningsList = action.warningList)
is SendAction.Warnings.Set -> sendState.copy(sendWarningsList = action.warningList)
else -> return sendState
}
@ -60,14 +60,15 @@ private class EmptyReducer : SendInternalReducer {
private class PrepareSendScreenStatesReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
val prepareAction = action as PrepareSendScreen
val walletManager = store.state.globalState.scanNoteResponse!!.walletManager!!
val walletManager = action.walletManager
?: store.state.globalState.scanNoteResponse!!.walletManager!!
val amountToExtract = prepareAction.tokenAmount ?: prepareAction.coinAmount!!
val decimals = amountToExtract.decimals
return sendState.copy(
walletManager = walletManager,
coinConverter = action.coinRate?.let { CurrencyConverter(it, decimals) },
tokenConverter = action.tokenRate?. let { CurrencyConverter(it, decimals) },
tokenConverter = action.tokenRate?.let { CurrencyConverter(it, decimals) },
amountState = sendState.amountState.copy(
amountToExtract = amountToExtract,
typeOfAmount = amountToExtract.type,

View file

@ -6,10 +6,10 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.TapCurrency
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.store
import org.rekotlin.StateType
import java.math.BigDecimal
@ -40,7 +40,7 @@ data class SendState(
val receiptState: ReceiptState = ReceiptState(),
val sendWarningsList: List<WarningMessage> = listOf(),
val sendButtonState: SendButtonState = SendButtonState.DISABLED,
val dialog: SendAction.Dialog? = null
val dialog: StateDialog? = null
) : SendScreenState {
override val stateId: StateId = StateId.SEND_SCREEN

View file

@ -23,7 +23,6 @@ import com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity
import com.tangem.tap.common.snackBar.MaxAmountSnackbar
import com.tangem.tap.common.text.truncateMiddleWith
import com.tangem.tap.common.toggleWidget.*
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
@ -235,8 +234,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
rv_warning_messages.addItemDecoration(SpacesItemDecoration(rv_warning_messages.dpToPx(16f).toInt()))
rv_warning_messages.adapter = warningsAdapter
val warnings = store.state.globalState.warningManager?.getWarnings(WarningMessage.Location.SendScreen) ?: listOf()
store.dispatch(SendAction.SetWarnings(warnings))
store.dispatch(SendAction.Warnings.Update)
}
override fun subscribeToStore() {

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.wallet.redux
import android.content.Context
import com.tangem.TangemError
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.AddressType
import com.tangem.commands.common.card.Card
@ -10,6 +9,7 @@ import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.wallet.R
import org.rekotlin.Action
@ -33,29 +33,18 @@ sealed class WalletAction : Action {
data class SetArtworkId(val artworkId: String?) : WalletAction()
// sealed class ProcessWallet : WalletAction() {
// data class LoadWallet(val artworkId: String?, val allowTopUp: Boolean
// ) : ProcessWallet() {
// data class Success(val wallet: Wallet) : ProcessWallet()
// data class NoAccount(val wallet: Wallet, val amountToCreateAccount: String) : ProcessWallet()
// data class Failure(val wallet: Wallet, val errorMessage: String? = null) : ProcessWallet()
// }
//
// data class UpdateWallet(val currency: CryptoCurrencyName? = null) : ProcessWallet() {
// object ScheduleUpdatingWallet : ProcessWallet()
// data class Success(val wallet: Wallet) : ProcessWallet()
// data class Failure(val errorMessage: String? = null) : ProcessWallet()
// }
// }
sealed class MultiWallet : WalletAction() {
data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet()
data class AddWalletManagers(val walletManagers: List<WalletManager>) : MultiWallet()
data class AddWalletManagers(val walletManagers: List<WalletManager>) : MultiWallet() {
constructor(walletManager: WalletManager) : this(listOf(walletManager))
}
data class AddBlockchain(val blockchain: Blockchain) : MultiWallet()
data class AddBlockchains(val blockchains: List<Blockchain>) : MultiWallet()
data class AddTokens(val tokens: List<Token>) : MultiWallet()
data class AddToken(val token: Token) : MultiWallet()
data class SaveCurrencies(val cardCurrencies: CardCurrencies) : MultiWallet()
object FindTokensInUse : MultiWallet()
data class FindBlockchainsInUse(val card: Card, val factory: WalletManagerFactory) : MultiWallet()
data class TokenLoaded(val amount: Amount) : MultiWallet()
data class SelectWallet(val walletData: WalletData?) : MultiWallet()
data class RemoveWallet(val walletData: WalletData) : MultiWallet()
@ -72,7 +61,8 @@ sealed class WalletAction : Action {
}
object CheckIfNeeded : Warnings()
data class SetWarnings(val warningList: List<WarningMessage>) : Warnings()
object Update : Warnings()
data class Set(val warningList: List<WarningMessage>) : Warnings()
object AppRating : Warnings() {
object SetNeverToShow : Warnings()
@ -99,7 +89,6 @@ sealed class WalletAction : Action {
}
object Scan : WalletAction()
class ScanCardFinished(val scanError: TangemError? = null) : WalletAction()
data class Send(val amount: Amount? = null) : WalletAction() {
data class ChooseCurrency(val amounts: List<Amount>?) : WalletAction()

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinCardNumber
@ -22,10 +23,9 @@ data class WalletState(
val error: ErrorType? = null,
val cardImage: Artwork? = null,
val hashesCountVerified: Boolean? = null,
val walletDialog: WalletDialog? = null,
val walletDialog: StateDialog? = null,
val twinCardsState: TwinCardsState? = null,
val mainWarningsList: List<WarningMessage> = mutableListOf(),
val scanCardFailsCounter: Int = 0,
val wallets: List<WalletData> = emptyList(),
val walletManagers: List<WalletManager> = emptyList(),
val isMultiwalletAllowed: Boolean = false,
@ -42,16 +42,26 @@ data class WalletState(
primaryWallet?.currencyData?.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard &&
primaryWallet?.currencyData?.status != com.tangem.tap.features.wallet.ui.BalanceStatus.UnknownBlockchain
val blockchains: List<Blockchain>
get() = walletManagers.map { it.wallet.blockchain }
fun getWalletManager(currencyName: CryptoCurrencyName?): WalletManager? {
if (currencyName == null) return null
val walletManager = walletManagers.find { it.wallet.blockchain.currency == currencyName }
if (walletManager != null) return walletManager
return walletManager ?: getWalletManagerForToken(currencyName)
}
fun getWalletManagerForToken(currencyName: CryptoCurrencyName?): WalletManager? {
val ethereumWalletManager = walletManagers.find { it.wallet.blockchain == Blockchain.Ethereum }
return if (ethereumWalletManager?.presetTokens?.find { it.symbol == currencyName } != null) {
ethereumWalletManager
} else {
null
val primaryWalletManager = walletManagers.find { it.wallet.blockchain == primaryBlockchain }
if (primaryWalletManager?.presetTokens?.find { it.symbol == currencyName } != null) {
primaryWalletManager
} else {
ethereumWalletManager
}
}
}
@ -96,7 +106,7 @@ data class WalletState(
}
sealed class WalletDialog {
sealed class WalletDialog: StateDialog {
data class QrDialog(
val qrCode: Bitmap?, val shareUrl: String?, val currencyName: CryptoCurrencyName?
) : WalletDialog()

View file

@ -1,10 +1,11 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TokenFinder
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -14,14 +15,19 @@ import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MultiWalletMiddleware {
fun handle(
action: WalletAction.MultiWallet, walletState: WalletState?, globalState: GlobalState?
action: WalletAction.MultiWallet, walletState: WalletState?, globalState: GlobalState?,
) {
when (action) {
is WalletAction.MultiWallet.AddWalletManagers -> {
val wallets = action.walletManagers.map { it.wallet }
store.state.globalState.feedbackManager?.infoHolder?.setWalletsInfo(wallets)
}
is WalletAction.MultiWallet.SelectWallet -> {
if (action.walletData != null) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletDetails))
@ -37,10 +43,19 @@ class MultiWalletMiddleware {
action.tokens.map { addToken(it, walletState) }
}
is WalletAction.MultiWallet.AddBlockchain -> {
globalState?.scanNoteResponse?.card?.cardId?.let {
currenciesRepository.saveAddedBlockchain(it, action.blockchain)
globalState?.scanNoteResponse?.card?.let { card ->
currenciesRepository.saveAddedBlockchain(card.cardId, action.blockchain)
globalState.tapWalletManager.walletManagerFactory
.makeWalletManager(card, action.blockchain)?.let {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(it))
}
}
store.dispatch(WalletAction.LoadFiatRate(currency = action.blockchain.currency))
store.dispatch(WalletAction.LoadWallet(currency = action.blockchain.currency))
}
is WalletAction.MultiWallet.SaveCurrencies -> {
val cardId = globalState?.scanNoteResponse?.card?.cardId
cardId?.let { currenciesRepository.saveCardCurrencies(it, action.cardCurrencies) }
}
is WalletAction.MultiWallet.RemoveWallet -> {
val cardId = globalState?.scanNoteResponse?.card?.cardId
@ -50,10 +65,39 @@ class MultiWalletMiddleware {
cardId?.let { currenciesRepository.removeBlockchain(it, action.walletData.blockchain) }
}
}
is WalletAction.MultiWallet.FindBlockchainsInUse -> {
val blockchains = currenciesRepository.getBlockchains()
.filterNot { walletState?.blockchains?.contains(it) == true }
val walletManagers = action.factory.makeWalletManagers(action.card, blockchains)
scope.launch {
walletManagers.map { walletManager ->
async(Dispatchers.IO) {
try {
walletManager.update()
val wallet = walletManager.wallet
val coinAmount = wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
if (walletState?.getWalletData(wallet.blockchain.currency) == null) {
scope.launch(Dispatchers.Main) {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(
listOfNotNull(walletManager)))
store.dispatch(WalletAction.MultiWallet.AddBlockchain(
wallet.blockchain
))
store.dispatch(WalletAction.LoadWallet.Success(wallet))
}
}
}
} catch (exception: Exception) {
}
}
}
}
}
is WalletAction.MultiWallet.FindTokensInUse -> {
val walletManager = walletState?.getWalletManager(Blockchain.Ethereum.currency)
?: return
val alreadyAddedTokens = walletManager.presetTokens
val tokenFinder = walletManager as TokenFinder
scope.launch {
val result = tokenFinder.findTokens()
@ -62,9 +106,7 @@ class MultiWalletMiddleware {
is Result.Success -> {
if (result.data.isNotEmpty()) {
store.dispatch(WalletAction.MultiWallet.AddTokens(
walletManager.presetTokens
.filterNot { presetToken -> alreadyAddedTokens.any { it.symbol == presetToken.symbol } }
.toList()
walletManager.presetTokens.toList()
))
}
}
@ -76,8 +118,7 @@ class MultiWalletMiddleware {
}
private fun addToken(token: Token, walletState: WalletState?) {
val walletManager =
(walletState?.getWalletManager(Blockchain.Ethereum.currency) as? EthereumWalletManager)
val walletManager = walletState?.getWalletManager(token.symbol)
scope.launch {
val result = walletManager?.addToken(token)
withContext(Dispatchers.Main) {

View file

@ -3,14 +3,15 @@ package com.tangem.tap.features.wallet.redux.middlewares
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat
import com.tangem.TangemSdkError
import com.tangem.blockchain.common.*
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.isZero
import com.tangem.tap.*
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.toSendableAmounts
@ -59,6 +60,13 @@ class WalletMiddleware {
}
}
is WalletAction.LoadWallet.Success -> {
val coinAmount = action.wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
if (walletState?.getWalletData(action.wallet.blockchain.currency) == null) {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(action.wallet.blockchain))
store.dispatch(WalletAction.LoadWallet.Success(action.wallet))
}
}
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
warningsMiddleware.tryToShowAppRatingWarning(action.wallet)
}
@ -126,27 +134,12 @@ class WalletMiddleware {
is WalletAction.Scan -> {
scope.launch {
val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler)
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
globalState?.tapWalletManager
?.onCardScanned(result.data, true)
if (walletState?.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
}
}
store.dispatch(WalletAction.ScanCardFinished())
}
is CompletionResult.Failure -> {
if (result.error !is TangemSdkError.UserCancelled) {
scope.launch(Dispatchers.Main) {
store.dispatch(WalletAction.ScanCardFinished(result.error))
if (store.state.walletState.scanCardFailsCounter >= 2) {
store.dispatch(WalletAction.ShowDialog.ScanFails)
}
}
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
globalState?.tapWalletManager?.onCardScanned(result.data, true)
}
}
}
@ -199,13 +192,14 @@ class WalletMiddleware {
private fun prepareSendAction(amount: Amount?, state: WalletState?): Action {
val selectedWalletData = state?.getSelectedWalletData()
val currency = selectedWalletData?.currencyData?.currencySymbol
val wallet = currency?.let { state.getWalletManager(currency)?.wallet }
val walletManager = state?.getWalletManager(currency)
val wallet = walletManager?.wallet
return if (amount != null) {
if (amount.type is AmountType.Token) {
prepareSendActionForToken(amount, state, selectedWalletData, wallet)
prepareSendActionForToken(amount, state, selectedWalletData, wallet, walletManager)
} else {
PrepareSendScreen(amount, selectedWalletData?.fiatRate)
PrepareSendScreen(amount, selectedWalletData?.fiatRate, walletManager)
}
} else {
val amounts = wallet?.amounts?.toSendableAmounts()
@ -213,23 +207,24 @@ class WalletMiddleware {
val amountToSend = amounts?.find { it.currencySymbol == currency }
?: return WalletAction.Send.ChooseCurrency(amounts)
if (amountToSend.type is AmountType.Token) {
prepareSendActionForToken(amount, state, selectedWalletData, wallet)
prepareSendActionForToken(amountToSend, state, selectedWalletData, wallet, walletManager)
} else {
PrepareSendScreen(amountToSend, selectedWalletData.fiatRate)
PrepareSendScreen(amountToSend, selectedWalletData.fiatRate, walletManager)
}
} else {
if (amounts?.size ?: 0 > 1) {
WalletAction.Send.ChooseCurrency(amounts)
} else {
val amountToSend = amounts?.first()
PrepareSendScreen(amountToSend, selectedWalletData?.fiatRate)
PrepareSendScreen(amountToSend, selectedWalletData?.fiatRate, walletManager)
}
}
}
}
private fun prepareSendActionForToken(
amount: Amount?, state: WalletState?, selectedWalletData: WalletData?, wallet: Wallet?
amount: Amount, state: WalletState?, selectedWalletData: WalletData?, wallet: Wallet?,
walletManager: WalletManager?
): PrepareSendScreen {
val coinRate = state?.getWalletData(wallet?.blockchain?.currency)?.fiatRate
val tokenRate = if (state?.isMultiwalletAllowed == true) {
@ -238,7 +233,7 @@ class WalletMiddleware {
selectedWalletData?.currencyData?.token?.fiatRate
}
return PrepareSendScreen(
wallet?.amounts?.get(AmountType.Coin), coinRate,
wallet?.amounts?.get(AmountType.Coin), coinRate, walletManager,
amount, tokenRate)
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardType
import com.tangem.commands.verifycard.VerifyCardState
import com.tangem.common.extensions.getType
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
@ -28,16 +29,9 @@ import java.math.BigDecimal
class WarningsMiddleware {
fun handle(action: WalletAction.Warnings, globalState: GlobalState?) {
when (action) {
WalletAction.Warnings.Update -> setWarningMessages()
is WalletAction.Warnings.CheckIfNeeded -> {
val validator = globalState?.scanNoteResponse?.walletManager as? SignatureCountValidator
globalState?.scanNoteResponse?.card?.let { card ->
globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local)
if (card.getType() != CardType.Release) addWarningMessage(WarningMessagesManager.devCardWarning())
if (!preferencesStorage.wasCardScannedBefore(card.cardId)) {
checkIfWarningNeeded(card, validator)?.let { addWarningMessage(it) }
}
updateWarningMessages()
}
showCardWarningsIfNeeded(globalState)
val readyToShow = preferencesStorage.appRatingLaunchObserver.isReadyToShow()
if (readyToShow) addWarningMessage(WarningMessagesManager.appRatingWarning(), true)
}
@ -68,6 +62,25 @@ class WarningsMiddleware {
}
}
private fun showCardWarningsIfNeeded(globalState: GlobalState?) {
val validator = globalState?.scanNoteResponse?.walletManager as? SignatureCountValidator
globalState?.scanNoteResponse?.card?.let { card ->
globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local)
if (card.getType() != CardType.Release) {
addWarningMessage(WarningMessagesManager.devCardWarning())
} else if (!preferencesStorage.wasCardScannedBefore(card.cardId)) {
checkIfWarningNeeded(card, validator)?.let { addWarningMessage(it) }
}
if (card.getType() == CardType.Release) {
if (globalState.scanNoteResponse.verifyResponse?.verificationState ==
VerifyCardState.VerifiedOffline) {
addWarningMessage(WarningMessagesManager.onlineVerificationFailed())
}
}
setWarningMessages()
}
}
private fun checkIfWarningNeeded(
card: Card, signatureCountValidator: SignatureCountValidator? = null,
): WarningMessage? {
@ -118,11 +131,15 @@ class WarningsMiddleware {
private fun addWarningMessage(warning: WarningMessage, autoUpdate: Boolean = false) {
store.state.globalState.warningManager?.addWarning(warning)
if (autoUpdate) updateWarningMessages()
if (autoUpdate) setWarningMessages()
}
private fun updateWarningMessages() {
val warningManager = store.state.globalState.warningManager ?: return
store.dispatch(WalletAction.Warnings.SetWarnings(warningManager.getWarnings(WarningMessage.Location.MainScreen)))
private fun setWarningMessages() {
store.dispatch(WalletAction.Warnings.Set(getWarnings()))
}
private fun getWarnings(): List<WarningMessage> {
val warningManager = store.state.globalState.warningManager ?: return emptyList()
return warningManager.getWarnings(WarningMessage.Location.MainScreen, store.state.walletState.blockchains)
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedCurrencyString
@ -19,7 +18,7 @@ class MultiWalletReducer {
return when (action) {
is WalletAction.MultiWallet.AddWalletManagers -> {
state.copy(
walletManagers = action.walletManagers
walletManagers = state.walletManagers + action.walletManagers
)
}
is WalletAction.MultiWallet.AddBlockchains -> {
@ -76,7 +75,6 @@ class MultiWalletReducer {
}
is WalletAction.MultiWallet.AddTokens -> {
if (!state.isMultiwalletAllowed) return state
val walletAddresses = createAddressList(state.getWalletManager(Blockchain.Ethereum.currency)?.wallet)
val wallets = action.tokens.map { token ->
WalletData(
currencyData = BalanceWidgetData(
@ -84,17 +82,23 @@ class MultiWalletReducer {
currency = token.name,
currencySymbol = token.symbol
),
walletAddresses = walletAddresses,
walletAddresses = createAddressList(
state.getWalletManagerForToken(token.symbol)?.wallet
),
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(allowed = false),
token = token
)
}
state.copy(wallets = state.wallets + wallets)
state.copy(wallets = state.replaceSomeWallets(wallets))
}
is WalletAction.MultiWallet.AddToken -> {
if (!state.isMultiwalletAllowed) return state
val walletAddresses = createAddressList(state.getWalletManager(Blockchain.Ethereum.currency)?.wallet)
val walletAddresses = createAddressList(
state.getWalletManagerForToken(action.token.symbol)?.wallet
)
val wallet = WalletData(
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
@ -110,21 +114,23 @@ class MultiWalletReducer {
state.copy(wallets = wallets)
}
is WalletAction.MultiWallet.TokenLoaded -> {
val pendingTransactions = state.getWalletManager(Blockchain.Ethereum.currency)
val pendingTransactions = state.getWalletManagerForToken(action.amount.currencySymbol)
?.wallet?.let { wallet ->
wallet.recentTransactions.toPendingTransactions(wallet.address)
} ?: emptyList()
val sendButtonEnabled = action.amount.value?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
val tokenPendingTransactions = pendingTransactions
.filter { it.currency == action.amount.currencySymbol }
val tokenBalanceStatus = when {
tokenPendingTransactions.isNotEmpty() -> BalanceStatus.TransactionInProgress
pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress
else -> BalanceStatus.VerifiedOnline
}
val tokenWalletData = state.getWalletData(action.amount.currencySymbol)
val newTokenWalletData = tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = balanceStatus,
status = tokenBalanceStatus,
amount = action.amount.value?.toFormattedCurrencyString(
action.amount.decimals, action.amount.currencySymbol
),
@ -156,6 +162,8 @@ class MultiWalletReducer {
is WalletAction.MultiWallet.SetPrimaryToken ->
state.copy(primaryToken = action.token)
is WalletAction.MultiWallet.FindTokensInUse -> state
is WalletAction.MultiWallet.FindBlockchainsInUse -> state
is WalletAction.MultiWallet.SaveCurrencies -> state
}
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.toFiatString
@ -21,112 +20,86 @@ import com.tangem.tap.store
import java.math.RoundingMode
class OnWalletLoadedReducer {
fun reduce(wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null): WalletState {
if (!walletState.isMultiwalletAllowed) {
return onSingleWalletLoaded(wallet, walletState, topUpAllowed)
return if (!walletState.isMultiwalletAllowed) {
onSingleWalletLoaded(wallet, walletState, topUpAllowed)
} else {
val fiatCurrencySymbol = store.state.globalState.appCurrency
val amount = wallet.amounts[AmountType.Coin]?.value
if (walletState.getWalletData(wallet.blockchain.currency) == null && amount?.isZero() != false) {
return walletState
}
if (wallet.blockchain != Blockchain.Ethereum) {
val formattedAmount = amount?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency)
val pendingTransactions = wallet.recentTransactions
.toPendingTransactions(wallet.address)
val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
}
val walletData = walletState.getWalletData(wallet.blockchain.currency)
?: WalletData()
val newWalletData = walletData.copy(
currencyData = walletData.currencyData.copy(
status = balanceStatus, currency = wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
amount = formattedAmount,
fiatAmount = walletData.fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) }
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
val wallets = walletState.replaceWalletInWallets(newWalletData)
val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
ProgressState.Done
}
return walletState.copy(
state = state, wallets = wallets
)
} else {
val formattedAmount = amount?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency)
val pendingTransactions = wallet.recentTransactions
.toPendingTransactions(wallet.address)
val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
}
val ethereumWalletData = walletState.getWalletData(wallet.blockchain.currency)
?: WalletData()
val newEthereumWalletData = ethereumWalletData.copy(
currencyData = ethereumWalletData.currencyData.copy(
status = balanceStatus,
amount = formattedAmount,
currency = wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
fiatAmount = ethereumWalletData.fiatRate?.let {
amount?.toFiatString(it, fiatCurrencySymbol)
}
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
val tokens = wallet.getTokens().mapNotNull { token ->
val tokenWalletData = walletState.getWalletData(token.symbol)
tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = balanceStatus,
amount = wallet.getTokenAmount(token)?.value?.toFormattedCurrencyString(
token.decimals, token.symbol
),
fiatAmount = tokenWalletData.fiatRate?.let {
wallet.getTokenAmount(token)?.value
?.toFiatString(it, fiatCurrencySymbol)
}
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
}
val wallets = walletState.replaceSomeWallets((tokens + newEthereumWalletData).filterNotNull())
val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
ProgressState.Done
}
return walletState.copy(
state = state, wallets = wallets
)
}
onMultiWalletLoaded(wallet, walletState, topUpAllowed)
}
}
private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null
private fun onMultiWalletLoaded(
wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null
): WalletState {
val fiatCurrencySymbol = store.state.globalState.appCurrency
val amount = wallet.amounts[AmountType.Coin]?.value
if (walletState.getWalletData(wallet.blockchain.currency) == null) {
return walletState
}
val formattedAmount = amount?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency)
val pendingTransactions = wallet.recentTransactions
.toPendingTransactions(wallet.address)
val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
}
val walletData = walletState.getWalletData(wallet.blockchain.currency)
?: WalletData()
val newWalletData = walletData.copy(
currencyData = walletData.currencyData.copy(
status = balanceStatus, currency = wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
amount = formattedAmount,
fiatAmount = walletData.fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) }
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
val tokens = wallet.getTokens().mapNotNull { token ->
val tokenWalletData = walletState.getWalletData(token.symbol)
val tokenPendingTransactions = pendingTransactions.filter { it.currency == token.symbol }
val tokenBalanceStatus = when {
tokenPendingTransactions.isNotEmpty() -> BalanceStatus.TransactionInProgress
pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress
else -> BalanceStatus.VerifiedOnline
}
tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = tokenBalanceStatus,
amount = wallet.getTokenAmount(token)?.value?.toFormattedCurrencyString(
token.decimals, token.symbol
),
fiatAmount = tokenWalletData.fiatRate?.let {
wallet.getTokenAmount(token)?.value
?.toFiatString(it, fiatCurrencySymbol)
}
),
pendingTransactions = tokenPendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
}
val wallets = walletState.replaceSomeWallets((tokens + newWalletData))
val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
ProgressState.Done
}
return walletState.copy(
state = state, wallets = wallets, error = null
)
}
private fun onSingleWalletLoaded(
wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null
): WalletState {
if (wallet.blockchain != walletState.primaryBlockchain) return walletState
@ -176,7 +149,7 @@ class OnWalletLoadedReducer {
)
val wallets = walletData?.let { listOf(walletData) } ?: emptyList()
return walletState.copy(
state = ProgressState.Done, wallets = wallets
state = ProgressState.Done, wallets = wallets, error = null
)
}
}

View file

@ -57,9 +57,16 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.LoadData.Failure -> {
when (action.error) {
is TapError.NoInternetConnection -> {
val wallets = newState.wallets
.map {
it.copy(currencyData = it.currencyData.copy(
status = BalanceStatus.Unreachable
))
}
newState = newState.copy(
state = ProgressState.Error,
error = ErrorType.NoInternetConnection,
wallets = wallets
)
}
is TapError.UnknownBlockchain -> {
@ -100,7 +107,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
} else {
val walletManager = newState.getWalletManager(action.currency) ?: return newState
val currencies = listOf(walletManager.wallet.blockchain.currency + walletManager.presetTokens.map { it.symbol })
val currencies = listOf(walletManager.wallet.blockchain.currency) + walletManager.presetTokens.map { it.symbol }
val newWallets = newState.wallets.filter { currencies.contains(it.currencyData.currencySymbol) }
.map { wallet ->
wallet.copy(
@ -118,14 +125,11 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
val wallets = newState.replaceSomeWallets(newWallets)
newState = newState.copy(wallets = wallets)
}
}
is WalletAction.LoadWallet.Success -> newState = onWalletLoadedReducer.reduce(action.wallet, newState)
is WalletAction.UpdateWallet.Success -> {
newState = onWalletLoadedReducer.reduce(action.wallet, newState)
// newState = newState.copy(updatingWallet = newState.pendingTransactions.isNotEmpty())
}
is WalletAction.LoadWallet.NoAccount -> {
val walletData = newState.getWalletData(action.wallet.blockchain.currency)?.copy(
currencyData = BalanceWidgetData(
@ -135,34 +139,46 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
)
val wallets = newState.replaceWalletInWallets(walletData)
val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
val progressState = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
ProgressState.Done
}
newState = newState.copy(
state = state,
state = progressState,
wallets = wallets
)
}
is WalletAction.LoadWallet.Failure -> {
val message = if (newState.error == ErrorType.NoInternetConnection) {
null
} else {
action.errorMessage
}
val walletData = newState.getWalletData(action.wallet.blockchain.currency)
val newWalletData = walletData?.copy(
currencyData = walletData.currencyData.copy(
status = BalanceStatus.Unreachable,
errorMessage = action.errorMessage
errorMessage = message
),
topUpState = TopUpState(false)
)
val wallets = newState.replaceWalletInWallets(newWalletData)
val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
val tokenWallets = action.wallet.getTokens()
.mapNotNull { newState.getWalletData(it.symbol) }
.map {
it.copy(currencyData = it.currencyData.copy(
status = BalanceStatus.Unreachable, errorMessage = message
))
}
val wallets = newState.replaceSomeWallets(listOfNotNull(newWalletData) + tokenWallets)
val progressState = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
ProgressState.Done
}
newState = newState.copy(
state = state, wallets = wallets
state = progressState, wallets = wallets
)
}
is WalletAction.SetArtworkId -> {
@ -174,21 +190,6 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
newState = newState.copy(cardImage = cardImage)
}
// is WalletAction.UpdateWallet -> {
// if (store.state.walletState.state == ProgressState.Done) {
// newState = newState.copy(updatingWallet = true)
// }
// }
// is WalletAction.UpdateWallet.ScheduleUpdatingWallet ->
// newState = newState.copy(updatingWallet = true)
// is WalletAction.UpdateWallet.Failure -> newState = newState.copy(updatingWallet = false)
// is WalletAction.LoadFiatRate -> {
// newState.copy(currencyData = newState.currencyData.copy(
// fiatAmount = null,
// token = newState.currencyData.token?.copy(fiatAmount = null))
// )
// }
is WalletAction.LoadFiatRate.Success ->
newState = setNewFiatRate(action.fiatRate, state.globalState.appCurrency, newState)
is WalletAction.LoadArtwork -> {
@ -246,13 +247,6 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
newState = newState.copy(wallets = wallets)
}
is WalletAction.ScanCardFinished -> {
newState = if (action.scanError == null) {
newState.copy(scanCardFailsCounter = 0)
} else {
newState.copy(scanCardFailsCounter = newState.scanCardFailsCounter + 1)
}
}
}
return newState
}
@ -282,16 +276,10 @@ fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null)
private fun handleCheckSignedHashesActions(action: WalletAction.Warnings, state: WalletState): WalletState {
return when (action) {
WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline -> state
WalletAction.Warnings.CheckHashesCount.ConfirmHashesCount -> state.copy(hashesCountVerified = true)
WalletAction.Warnings.CheckHashesCount.NeedToCheckHashesCountOnline -> state.copy(hashesCountVerified = false)
WalletAction.Warnings.CheckHashesCount.SaveCardId -> state
is WalletAction.Warnings.SetWarnings -> state.copy(mainWarningsList = action.warningList)
WalletAction.Warnings.CheckIfNeeded -> state
WalletAction.Warnings.AppRating -> state
WalletAction.Warnings.CheckHashesCount -> state
WalletAction.Warnings.AppRating.RemindLater -> state
WalletAction.Warnings.AppRating.SetNeverToShow -> state
is WalletAction.Warnings.Set -> state.copy(mainWarningsList = action.warningList)
else -> state
}
}

View file

@ -13,6 +13,7 @@ import java.math.BigDecimal
enum class BalanceStatus {
VerifiedOnline,
TransactionInProgress,
SameCurrencyTransactionInProgress,
Unreachable,
Loading,
NoAccount,

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType
import com.tangem.blockchain.blockchains.cardano.CardanoAddressType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressType
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.wallet.R
class MultipleAddressUiHelper {
@ -20,13 +19,6 @@ class MultipleAddressUiHelper {
}
}
fun idToType(id: Int, currencyName: CryptoCurrencyName?): AddressType? {
val blockchain = currencyName?.let { currency ->
Blockchain.fromCurrency(currency)
}
return idToType(id, blockchain)
}
fun idToType(id: Int, blockchain: Blockchain?): AddressType? {
return when (id) {
R.id.chip_default -> {

View file

@ -10,21 +10,18 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.MainActivity
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletDialog
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_twin_cards.*
import kotlinx.android.synthetic.main.fragment_wallet.*
import kotlinx.android.synthetic.main.fragment_wallet_details.*
import kotlinx.android.synthetic.main.fragment_wallet_details.rv_pending_transaction
import kotlinx.android.synthetic.main.fragment_wallet_details.toolbar
import kotlinx.android.synthetic.main.item_currency_wallet.view.*
import kotlinx.android.synthetic.main.layout_balance_error.*
@ -104,6 +101,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
showPendingTransactionsIfPresent(selectedWallet.pendingTransactions)
setupAddressCard(selectedWallet)
setupNoInternetHandling(state)
setupBalanceData(selectedWallet.currencyData)
btn_confirm.isEnabled = selectedWallet.mainButton.enabled
@ -156,7 +154,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
private fun setupAddressCard(state: WalletData) {
if (state.walletAddresses != null) {
if (state.shouldShowMultipleAddress()) {
if (state.shouldShowMultipleAddress() && state.blockchain != null) {
(card_balance as? ViewGroup)?.beginDelayedTransition()
chip_group_address_type.show()
chip_group_address_type.fitChipsByGroupWidth()
@ -166,7 +164,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
chip_group_address_type.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
val type = MultipleAddressUiHelper.idToType(checkedId, state.currencyData.currencySymbol)
val type = MultipleAddressUiHelper.idToType(checkedId, state.blockchain)
type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) }
}
} else {
@ -178,7 +176,21 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
state.walletAddresses.selectedAddress.exploreUrl,
requireContext()))
}
iv_qr_code.setImageBitmap(state.walletAddresses.selectedAddress.address.toQrCode())
iv_qr_code.setImageBitmap(state.walletAddresses.selectedAddress.shareUrl.toQrCode())
}
}
private fun setupNoInternetHandling(state: WalletState) {
if (state.state == ProgressState.Error) {
if (state.error == ErrorType.NoInternetConnection) {
srl_wallet_details?.isRefreshing = false
(activity as? MainActivity)?.showSnackbar(
text = R.string.wallet_notification_no_internet,
buttonTitle = R.string.common_retry
) { store.dispatch(WalletAction.LoadData) }
}
} else {
(activity as? MainActivity)?.dismissSnackbar()
}
}
@ -193,10 +205,12 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
showStatus(R.id.tv_status_loading)
showBalanceWithoutToken(data, false)
}
BalanceStatus.VerifiedOnline, BalanceStatus.TransactionInProgress -> {
BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress,
BalanceStatus.TransactionInProgress -> {
l_balance.show()
l_balance_error.hide()
val statusView = if (data.status == BalanceStatus.VerifiedOnline) {
val statusView = if (data.status == BalanceStatus.VerifiedOnline ||
data.status == BalanceStatus.SameCurrencyTransactionInProgress) {
R.id.tv_status_verified
} else {
tv_status_error.text =
@ -233,6 +247,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
)
}
}
card_pending_transaction_warning.show(data.status == BalanceStatus.SameCurrencyTransactionInProgress)
}
private fun showStatus(@IdRes viewRes: Int) {
@ -249,7 +264,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
}
}
private fun handleDialogs(walletDialog: WalletDialog?) {
private fun handleDialogs(walletDialog: StateDialog?) {
when (walletDialog) {
is WalletDialog.SelectAmountToSendDialog -> {
if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply {

View file

@ -12,9 +12,9 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.squareup.picasso.Picasso
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.MainActivity
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -30,13 +30,13 @@ import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_wallet.*
import kotlinx.android.synthetic.main.fragment_wallet.toolbar
import kotlinx.android.synthetic.main.fragment_wallet_details.*
import org.rekotlin.StoreSubscriber
class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<WalletState> {
private var snackbar: Snackbar? = null
private lateinit var warningsAdapter: WarningMessagesAdapter
private var walletView: WalletView = SingleWalletView()
@ -133,15 +133,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
private fun setupNoInternetHandling(state: WalletState) {
if (state.state == ProgressState.Error) {
if (state.error == ErrorType.NoInternetConnection) {
srl_wallet?.isRefreshing = false
snackbar = Snackbar.make(
coordinator_wallet, getString(R.string.wallet_notification_no_internet),
Snackbar.LENGTH_INDEFINITE
).setAction(getString(R.string.common_retry)) { store.dispatch(WalletAction.LoadData) }
snackbar?.show()
srl_wallet_details?.isRefreshing = false
(activity as? MainActivity)?.showSnackbar(
text = R.string.wallet_notification_no_internet,
buttonTitle = R.string.common_retry
) { store.dispatch(WalletAction.LoadData) }
}
} else {
snackbar?.dismiss()
(activity as? MainActivity)?.dismissSnackbar()
}
}

View file

@ -8,7 +8,9 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.getIconRes
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.ui.BalanceStatus
@ -80,11 +82,8 @@ class WalletAdapter
fun bind(wallet: WalletData) {
view.tv_currency.text = wallet.currencyData.currency
view.tv_amount.text = wallet.currencyData.amount
if (view.tv_amount.isEllipsized()) {
val allowedSize = view.tv_amount.text.length - 4
view.tv_amount.text = wallet.currencyData.amount?.ellipsizeBeforeSpace(allowedSize)
}
view.tv_amount.text = wallet.currencyData.amount?.takeWhile { !it.isWhitespace() }
view.tv_currency_symbol.text = wallet.currencyData.amount?.takeLastWhile { !it.isWhitespace() }
view.tv_amount_fiat.text = wallet.currencyData.fiatAmount
view.tv_exchange_rate.text = wallet.fiatRateString
view.card_wallet.setOnClickListener {
@ -101,7 +100,7 @@ class WalletAdapter
view.iv_currency.setImageResource(R.drawable.shape_circle)
}
when (wallet.currencyData.status) {
BalanceStatus.VerifiedOnline -> hideWarning()
BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress -> hideWarning()
BalanceStatus.Loading -> {
hideWarning()
if (wallet.currencyData.amount == null) {

View file

@ -4,6 +4,7 @@ import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.feedback.ScanFailsEmail
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -22,7 +23,10 @@ class ScanFailsDialog {
store.dispatch(GlobalAction.SendFeedback(ScanFailsEmail()))
}
setNegativeButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatch(WalletAction.HideDialog) }
setOnDismissListener {
store.dispatch(HomeAction.HideDialog)
store.dispatch(WalletAction.HideDialog)
}
}.create()
}
}

View file

@ -4,6 +4,7 @@ import android.app.Dialog
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.tokens.redux.TokensAction
@ -112,7 +113,7 @@ class MultiWalletView : WalletView {
}
private fun showErrorState(
fragment: WalletFragment, errorTitle: CharSequence, errorDescription: CharSequence
fragment: WalletFragment, errorTitle: CharSequence, errorDescription: CharSequence,
) {
fragment.l_card_balance.show()
fragment.l_balance.hide()
@ -131,14 +132,12 @@ class MultiWalletView : WalletView {
fragment.btn_confirm_long.text = fragment.getText(R.string.wallet_button_create_wallet)
}
private fun handleDialogs(walletDialog: WalletDialog?) {
private fun handleDialogs(walletDialog: StateDialog?) {
val fragment = fragment ?: return
val context = fragment.context ?: return
when (walletDialog) {
is WalletDialog.ScanFailsDialog -> {
if (dialog == null) dialog = ScanFailsDialog.create(context).apply {
this.show()
}
if (dialog == null) dialog = ScanFailsDialog.create(context).apply { show() }
}
else -> {
dialog?.dismiss()

View file

@ -6,6 +6,9 @@ import android.view.ViewGroup
import android.widget.Button
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
@ -105,6 +108,9 @@ class SingleWalletView : WalletView {
this.tv_twin_card_number.hide()
this.iv_twin_card.hide()
}
if (twinCardsState?.showTwinOnboarding == true) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
}
}
}
@ -156,7 +162,7 @@ class SingleWalletView : WalletView {
}
private fun setupConfirmButton(
state: WalletData, btnConfirm: Button, fragment: WalletFragment, isTwinsWallet: Boolean
state: WalletData, btnConfirm: Button, fragment: WalletFragment, isTwinsWallet: Boolean,
) {
val buttonTitle = when (state.mainButton) {
is WalletMainButton.SendButton -> R.string.wallet_button_send
@ -181,7 +187,7 @@ class SingleWalletView : WalletView {
private fun setupAddressCard(state: WalletData) {
val fragment = fragment ?: return
if (state.walletAddresses != null) {
if (state.walletAddresses != null && state.blockchain != null) {
fragment.l_address?.show()
if (state.shouldShowMultipleAddress()) {
(fragment.l_address as? ViewGroup)?.beginDelayedTransition()
@ -193,7 +199,7 @@ class SingleWalletView : WalletView {
fragment.chip_group_address_type.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
val type = MultipleAddressUiHelper.idToType(checkedId, state.currencyData.currencySymbol)
val type = MultipleAddressUiHelper.idToType(checkedId, state.blockchain)
type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) }
}
} else {
@ -210,7 +216,7 @@ class SingleWalletView : WalletView {
}
}
private fun handleDialogs(walletDialog: WalletDialog?) {
private fun handleDialogs(walletDialog: StateDialog?) {
val fragment = fragment ?: return
val context = fragment.context ?: return
when (walletDialog) {
@ -229,9 +235,7 @@ class SingleWalletView : WalletView {
}
}
is WalletDialog.ScanFailsDialog -> {
if (dialog == null) dialog = ScanFailsDialog.create(context).apply {
this.show()
}
if (dialog == null) dialog = ScanFailsDialog.create(context).apply { show() }
}
null -> {
dialog?.dismiss()

View file

@ -122,7 +122,10 @@ class AppRatingLaunchObserver(
if (fundsFoundDate != null) return
fundsFoundDate = Calendar.getInstance()
preferences.edit().putLong(K_FUNDS_FOUND_DATE, fundsFoundDate!!.timeInMillis).apply()
preferences.edit(true) {
putLong(K_FUNDS_FOUND_DATE, fundsFoundDate!!.timeInMillis).apply()
putInt(K_SHOW_RATING_AT_LAUNCH_COUNT, launchCounts + firstShowing)
}
}
fun isReadyToShow(): Boolean {
@ -130,8 +133,8 @@ class AppRatingLaunchObserver(
if (!userWasInteractWithRating()) {
val diff = Calendar.getInstance().timeInMillis - fundsDate.timeInMillis
val diffInDays = diff / (100 * 60 * 60 * 24)
if (diffInDays >= firstShowing) return true
val diffInDays = diff / (1000 * 60 * 60 * 24)
return launchCounts >= getCounterOfNextShowing() && diffInDays >= firstShowing
}
val nextShowing = getCounterOfNextShowing()

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -258,48 +258,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_security_title" />
<TextView
android:id="@+id/tv_blockchain_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="22dp"
android:paddingBottom="4dp"
android:text="@string/details_section_title_blockchain"
android:textAllCaps="true"
android:textColor="@color/colorSecondary"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_erase_wallet" />
<View
android:id="@+id/v_separator_blockchain"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/separatorGrey2"
app:layout_constraintTop_toBottomOf="@id/tv_blockchain_title" />
<TextView
android:id="@+id/tv_manage_tokens"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/details_row_title_manage_tokens"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_blockchain_title" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_blockchain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="tv_blockchain_title, v_separator_blockchain, tv_manage_tokens" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -171,6 +171,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="30dp"
app:layout_constraintTop_toBottomOf="@id/l_address"
app:layout_constraintVertical_bias="1"
app:layout_constraintBottom_toBottomOf="parent" />
<include
@ -179,6 +182,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="30dp"
app:layout_constraintTop_toBottomOf="@id/l_address"
app:layout_constraintVertical_bias="1"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.button.MaterialButton

View file

@ -79,6 +79,36 @@
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_pending_transaction_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:cardBackgroundColor="@color/darkGray1"
app:cardCornerRadius="8dp"
app:layout_constraintTop_toBottomOf="@+id/l_wallet_details">
<TextView
android:padding="16dp"
android:id="@+id/tv_pending_transaction_warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:text="@string/wallet_pending_transaction_warning"
android:textColor="@android:color/white" />
</com.google.android.material.card.MaterialCardView>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="l_wallet_details,card_pending_transaction_warning" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_top_up"
style="@style/TapButtonWithIcon"
@ -86,11 +116,12 @@
android:layout_marginStart="16dp"
android:layout_marginTop="30dp"
android:layout_marginBottom="33dp"
android:layout_marginEnd="4dp"
android:text="@string/wallet_button_topup"
app:icon="@drawable/ic_arrow_up"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="1" />
@ -107,12 +138,13 @@
android:id="@+id/btn_confirm"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_marginStart="8dp"
android:layout_marginStart="4dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="33dp"
android:text="@string/wallet_button_send"
app:icon="@drawable/ic_send"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline"

View file

@ -17,7 +17,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="76dp">
<ImageView
android:id="@+id/iv_currency"
@ -49,44 +49,48 @@
<TextView
android:id="@+id/tv_currency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="24dp"
android:layout_marginTop="16dp"
android:paddingStart="16dp"
android:paddingEnd="2dp"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="parent"
tools:text="Bitcoin" />
<TextView
android:id="@+id/tv_exchange_rate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="20dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:layout_marginBottom="16dp"
android:textColor="@color/darkGray2"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@id/iv_currency"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_currency"
tools:text="USD 3 588" />
<TextView
android:id="@+id/tv_status_error_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="20dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:layout_marginBottom="16dp"
android:textColor="@color/warning"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/iv_currency"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_currency" />
<TextView
android:id="@+id/tv_status_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="20dp"
android:layout_marginBottom="16dp"
android:drawablePadding="5dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
@ -94,37 +98,56 @@
android:textColor="@color/darkGray6"
android:textSize="13sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/iv_currency"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_amount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="24dp"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:paddingStart="2dp"
android:paddingEnd="16dp"
android:layout_marginStart="2dp"
android:layout_marginTop="16dp"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/tv_currency_symbol"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="@id/tv_currency"
app:layout_constraintTop_toTopOf="parent"
tools:text="3 588" />
<TextView
android:id="@+id/tv_currency_symbol"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:gravity="end"
android:maxLines="1"
android:layout_marginStart="4dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="@id/tv_currency"
app:layout_constraintTop_toTopOf="@id/tv_currency"
tools:text="3 588 BTC" />
app:layout_constraintStart_toEndOf="@id/tv_amount"
app:layout_constraintTop_toTopOf="parent"
tools:text="BTC" />
<TextView
android:id="@+id/tv_amount_fiat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="20dp"
android:layout_marginBottom="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textColor="@color/darkGray2"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tv_exchange_rate"
tools:text="0.43 USD" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -83,7 +83,7 @@
app:layout_constraintEnd_toEndOf="@id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/chip_group_address_type"
app:layout_constraintBottom_toBottomOf="@id/btn_copy"
app:layout_constraintBottom_toBottomOf="@id/v_qr_lower_end"
tools:src="@drawable/ic_qr_code_show" />
<androidx.constraintlayout.widget.Guideline
@ -98,7 +98,7 @@
android:id="@+id/tv_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="14dp"
android:ellipsize="middle"
android:maxLines="2"
@ -148,6 +148,15 @@
app:layout_constraintEnd_toStartOf="@id/btn_share"
app:layout_constraintStart_toEndOf="@id/iv_qr_code" />
<View
android:id="@+id/v_qr_lower_end"
android:layout_width="1dp"
android:layout_height="1dp"
app:layout_constraintTop_toBottomOf="@id/btn_copy"
app:layout_constraintStart_toStartOf="@id/btn_copy"
android:layout_marginTop="5dp"
/>
<com.google.android.material.chip.Chip
android:id="@+id/btn_share"

View file

@ -97,8 +97,10 @@ this wallet.
<string name="wallet_address_button_copy" translatable="false">Copy</string>
<string name="wallet_address_button_share" translatable="false">Share</string>
<string name="wallet_button_add_tokens" translatable="false">+ Add tokens</string>
<string name="wallet_pending_transaction_warning" translatable="false">Please wait for the transaction to complete to be able to send funds</string>
<string name="warning_failed_to_verify_card_title" translatable="false">Authenticity check failed</string>
<string name="warning_failed_to_verify_card_message" translatable="false">This card might be a production sample or counterfeit</string>