diff --git a/app/build.gradle b/app/build.gradle index 549774d931..76faa09bc5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -69,21 +69,25 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'com.google.android.material:material:1.2.1' - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' + implementation 'com.google.android.material:material:1.3.0' + implementation "androidx.core:core-ktx:1.3.2" + implementation 'com.google.android.play:core:1.10.0' + implementation 'com.google.android.play:core-ktx:1.8.1' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation 'com.tangem:blockchain:1.141.0' - implementation 'com.tangem:core:1.101.0' - implementation 'com.tangem:sdk:1.101.0' + + implementation 'com.tangem:blockchain:1.166.0' + implementation 'com.tangem:core:1.109.0' + implementation 'com.tangem:sdk:1.109.0' // WebView implementation "androidx.browser:browser:1.3.0" //lifecycle - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" - implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" + implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" implementation "org.rekotlin:rekotlin:1.0.4" @@ -103,7 +107,7 @@ dependencies { implementation 'com.google.firebase:firebase-config-ktx' implementation 'com.google.firebase:firebase-analytics-ktx' - testImplementation 'junit:junit:4.13.1' + testImplementation 'junit:junit:4.13.2' testImplementation "com.google.truth:truth:1.0.1" androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml index 4cc2b19474..bf9ef4d842 100644 --- a/app/src/debug/res/values/strings.xml +++ b/app/src/debug/res/values/strings.xml @@ -1,6 +1,6 @@ - DEBUG + DebugTangem \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2b7b764bfc..ee1acb1fe8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -81,6 +81,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/assets/erc20_tokens.json b/app/src/main/assets/erc20_tokens.json new file mode 100644 index 0000000000..9b29c198bb --- /dev/null +++ b/app/src/main/assets/erc20_tokens.json @@ -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" + } +] diff --git a/app/src/main/assets/tangem-android-config b/app/src/main/assets/tangem-android-config new file mode 160000 index 0000000000..9a731fcb5a --- /dev/null +++ b/app/src/main/assets/tangem-android-config @@ -0,0 +1 @@ +Subproject commit 9a731fcb5a58d53c90d7db79e5441bdf18e087fc diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 2a20c88628..78c6c7dbd6 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -5,10 +5,12 @@ import android.content.pm.ActivityInfo 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.Log import com.tangem.TangemSdk import com.tangem.commands.common.card.CardType import com.tangem.tangem_sdk_new.extensions.init @@ -46,15 +48,19 @@ private fun initCoroutineExceptionHandler(): CoroutineExceptionHandler { val sw = StringWriter() throwable.printStackTrace(PrintWriter(sw)) val exceptionAsString: String = sw.toString() - Log.e("TangemSdk", exceptionAsString) + Log.e("Coroutine", exceptionAsString) throw throwable } } 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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index cf26114c15..e458fd4e04 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -1,14 +1,23 @@ package com.tangem.tap import android.app.Application +import com.google.firebase.ktx.Firebase +import com.google.firebase.remoteconfig.ktx.remoteConfig +import com.google.firebase.remoteconfig.ktx.remoteConfigSettings +import com.tangem.Log import com.tangem.tap.common.images.PicassoHelper import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.config.ConfigLoader -import com.tangem.tap.domain.config.ConfigManager -import com.tangem.tap.domain.config.LocalLoader -import com.tangem.tap.domain.config.RemoteLoader +import com.tangem.tap.domain.configurable.config.ConfigManager +import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader +import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader +import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader +import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager +import com.tangem.tap.domain.tokens.CurrenciesRepository +import com.tangem.tap.features.feedback.AdditionalEmailInfo +import com.tangem.tap.features.feedback.FeedbackManager +import com.tangem.tap.features.feedback.TangemLogCollector import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.network.createMoshi import com.tangem.tap.persistence.PreferencesStorage @@ -22,27 +31,49 @@ val store = Store( state = AppState() ) lateinit var preferencesStorage: PreferencesStorage +lateinit var currenciesRepository: CurrenciesRepository class TapApplication : Application() { override fun onCreate() { super.onCreate() - if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings { + this.minimumFetchIntervalInSeconds = 60 + }) + } else { + Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings { + this.minimumFetchIntervalInSeconds = 3600 + }) + } - ConfigLoader.init() NetworkConnectivity.createInstance(store, this) preferencesStorage = PreferencesStorage(this) PicassoHelper.initPicassoWithCaching(this) + currenciesRepository = CurrenciesRepository(this) + initFeedbackManager() loadConfigs() } - private fun loadConfigs() { val moshi = createMoshi() - val localLoader = LocalLoader(this, moshi) - val remoteLoader = RemoteLoader(moshi) + val localLoader = FeaturesLocalLoader(this, moshi) + val remoteLoader = FeaturesRemoteLoader(moshi) val configManager = ConfigManager(localLoader, remoteLoader) configManager.load { store.dispatch(GlobalAction.SetConfigManager(configManager)) } + val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi)) + warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) } + } + + private fun initFeedbackManager() { + val infoHolder = AdditionalEmailInfo() + infoHolder.setAppVersion(this) + + val logWriter = TangemLogCollector() + Log.addLogger(logWriter) + + store.dispatch(GlobalAction.SetFeedbackManager(FeedbackManager(infoHolder, logWriter))) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEvent.kt index 932897e204..4395bd0922 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEvent.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEvent.kt @@ -4,4 +4,8 @@ enum class AnalyticsEvent(val event: String) { CARD_IS_SCANNED("card_is_scanned"), TRANSACTION_IS_SENT("transaction_is_sent"), READY_TO_SCAN("ready_to_scan"), + APP_RATING_DISPLAYED("rate_app_warning_displayed"), + APP_RATING_DISMISS("dismiss_rate_app_warning"), + APP_RATING_NEGATIVE("negative_rate_app_feedback"), + APP_RATING_POSITIVE("positive_rate_app_feedback"), } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt index 9dddeee4f2..1dc3586808 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt @@ -4,5 +4,5 @@ import com.tangem.commands.common.card.Card interface AnalyticsHandler { - fun triggerEvent(event: AnalyticsEvent, card: Card?) + fun triggerEvent(event: AnalyticsEvent, card: Card? = null) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt index 0d49728dd6..d1d3ce7a38 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt @@ -8,8 +8,7 @@ import com.tangem.commands.common.card.Card object FirebaseAnalyticsHandler : AnalyticsHandler { override fun triggerEvent(event: AnalyticsEvent, card: Card?) { - Firebase.analytics - .logEvent(event.event, setCardData(card)) + Firebase.analytics.logEvent(event.event, setCardData(card)) } fun logException(name: String, throwable: Throwable) { diff --git a/app/src/main/java/com/tangem/tap/common/extensions/AssetManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/AssetManager.kt new file mode 100644 index 0000000000..b2905ec473 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/AssetManager.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.common.extensions + +import android.content.res.AssetManager + +fun AssetManager.readJsonFileToString(fileName: String): String = + this.open("$fileName.json").bufferedReader().readText() \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt new file mode 100644 index 0000000000..dc12172a82 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.common.extensions + +import androidx.annotation.DrawableRes +import com.tangem.blockchain.common.Blockchain +import com.tangem.wallet.R + +@DrawableRes +fun Blockchain.getIconRes(): Int { + return when (this) { + 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, Blockchain.EthereumTestnet, -> R.drawable.ic_eth + Blockchain.RSK -> R.drawable.ic_rsk + Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Collections.kt b/app/src/main/java/com/tangem/tap/common/extensions/Collections.kt new file mode 100644 index 0000000000..a280071a02 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Collections.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.common.extensions + +/** +[REDACTED_AUTHOR] + */ +fun List.containsAny(list: List): Boolean { + this.forEach { mainItem -> + list.forEach { if (it == mainItem) return true } + } + return false +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 56f0d70061..6e348ea8cf 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -12,6 +12,8 @@ import com.tangem.tap.features.details.ui.twins.TwinWalletWarningFragment import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment import com.tangem.tap.features.home.HomeFragment import com.tangem.tap.features.send.ui.SendFragment +import com.tangem.tap.features.tokens.ui.AddTokensFragment +import com.tangem.tap.features.wallet.ui.WalletDetailsFragment import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.tap.features.wallet.ui.dialogs.TwinsOnboardingFragment import com.tangem.wallet.R @@ -46,5 +48,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.CreateTwinWalletWarning -> TwinWalletWarningFragment() AppScreen.CreateTwinWallet -> CreateTwinWalletFragment() AppScreen.TwinsOnboarding -> TwinsOnboardingFragment() + AppScreen.AddTokens -> AddTokensFragment() + AppScreen.WalletDetails -> WalletDetailsFragment() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt index 42fc659da8..8ed277a345 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt @@ -1,11 +1,5 @@ package com.tangem.tap.common.extensions -import android.graphics.Bitmap -import android.graphics.Color -import com.google.zxing.BarcodeFormat -import com.google.zxing.EncodeHintType -import com.google.zxing.qrcode.QRCodeWriter -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import com.tangem.common.extensions.isZero import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.network.coinmarketcap.FiatCurrency @@ -15,27 +9,6 @@ import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.* - -fun String.toQrCode(): Bitmap { - val hintMap = Hashtable() - hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage - hintMap[EncodeHintType.MARGIN] = 2 - - val qrCodeWriter = QRCodeWriter() - - val size = 256 - - val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, size, size, hintMap) - val width = bitMatrix.width - val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565) - for (x in 0 until width) { - for (y in 0 until width) { - bmp.setPixel(y, x, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE) - } - } - return bmp -} - fun BigDecimal.toFormattedString( decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN, locale: Locale = Locale.US ): String { diff --git a/app/src/main/java/com/tangem/tap/common/extensions/String.kt b/app/src/main/java/com/tangem/tap/common/extensions/String.kt new file mode 100644 index 0000000000..f0f4ef1d00 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/String.kt @@ -0,0 +1,62 @@ +package com.tangem.tap.common.extensions + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color +import android.text.Spannable +import android.text.style.ForegroundColorSpan +import androidx.core.content.ContextCompat +import androidx.core.text.toSpannable +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.qrcode.QRCodeWriter +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import java.util.* + +fun String?.ellipsizeBeforeSpace(allowedSize: Int): String { + if (this.isNullOrBlank()) return "" + val size = this.length + val sizeDifference = size - allowedSize + val endIndex = this.indexOf(" ") + val startIndex = endIndex - sizeDifference + val newString = this.removeRange(startIndex, endIndex) + return newString.substring(0 until startIndex) + "..." + + newString.substring(startIndex until newString.length) +} + +fun String.colorSegment( + context: Context, + color: Int, + startIndex: Int = 0, + endIndex: Int = this.length +): Spannable { + return this.toSpannable() + .also { spannable -> + spannable.setSpan( + ForegroundColorSpan(ContextCompat.getColor(context, color)), + startIndex, + endIndex, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } +} + +fun String.toQrCode(): Bitmap { + val hintMap = Hashtable() + hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage + hintMap[EncodeHintType.MARGIN] = 2 + + val qrCodeWriter = QRCodeWriter() + + val size = 256 + + val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, size, size, hintMap) + val width = bitMatrix.width + val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565) + for (x in 0 until width) { + for (y in 0 until width) { + bmp.setPixel(y, x, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE) + } + } + return bmp +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt b/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt index 5774e94b61..48ee67afc7 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt @@ -56,4 +56,13 @@ fun TextInputLayout.enableError(enable: Boolean, errorMessage: String? = null) { error = null isErrorEnabled = false } +} + +fun TextView.isEllipsized(): Boolean { + val layout = this.layout + if (layout != null) { + val lines: Int = layout.lineCount + return (lines > 0) && (layout.getEllipsisCount(lines - 1) > 0) + } + return false } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Token.kt b/app/src/main/java/com/tangem/tap/common/extensions/Token.kt new file mode 100644 index 0000000000..fdfb83ef77 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Token.kt @@ -0,0 +1,16 @@ +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 try { + ("#" + this.contractAddress.subSequence(2..7).toString()) + .toColorInt() + } catch (exception: Exception) { + R.color.lightGray4 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index c7bb0daf0f..b43c49b307 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -1,15 +1,10 @@ package com.tangem.tap.common.extensions import android.app.Activity -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent +import android.content.* import android.content.res.Resources import android.graphics.drawable.Drawable import android.os.Build -import android.text.Spannable -import android.text.style.ForegroundColorSpan import android.util.TypedValue import android.view.View import android.view.ViewGroup @@ -19,7 +14,6 @@ import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.content.ContextCompat -import androidx.core.text.toSpannable import androidx.fragment.app.Fragment import com.google.android.material.card.MaterialCardView @@ -77,6 +71,9 @@ fun Context.dpToPixels(dp: Int): Int = TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics ).toInt() +tailrec fun Context?.getActivity(): Activity? = this as? Activity ?: + (this as? ContextWrapper)?.baseContext?.getActivity() + fun MaterialCardView.setMargins( marginLeftDp: Int = 16, marginTopDp: Int = 8, @@ -106,24 +103,6 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) { } } - -fun String.colorSegment( - context: Context, - color: Int, - startIndex: Int = 0, - endIndex: Int = this.length -): Spannable { - return this.toSpannable() - .also { spannable -> - spannable.setSpan( - ForegroundColorSpan(ContextCompat.getColor(context, color)), - startIndex, - endIndex, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } -} - fun View.hideKeyboard() { val inputMethodManager = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? InputMethodManager inputMethodManager?.hideSoftInputFromWindow(this.windowToken, 0) diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 5e80adec68..03dcb4a441 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -6,7 +6,8 @@ import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer import com.tangem.tap.features.home.redux.HomeReducer import com.tangem.tap.features.send.redux.reducers.SendScreenReducer -import com.tangem.tap.features.wallet.redux.WalletReducer +import com.tangem.tap.features.tokens.redux.TokensReducer +import com.tangem.tap.features.wallet.redux.reducers.WalletReducer import org.rekotlin.Action fun appReducer(action: Action, state: AppState?): AppState { @@ -20,7 +21,8 @@ fun appReducer(action: Action, state: AppState?): AppState { walletState = WalletReducer.reduce(action, state), sendState = SendScreenReducer.reduce(action, state.sendState), detailsState = DetailsReducer.reduce(action, state), - disclaimerState = DisclaimerReducer.reduce(action, state) + disclaimerState = DisclaimerReducer.reduce(action, state), + tokensState = TokensReducer.reduce(action, state), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index e60ae97fa7..4548ca046f 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -12,8 +12,10 @@ import com.tangem.tap.features.home.redux.HomeMiddleware import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.send.redux.middlewares.sendMiddleware import com.tangem.tap.features.send.redux.states.SendState -import com.tangem.tap.features.wallet.redux.WalletMiddleware +import com.tangem.tap.features.tokens.redux.TokensMiddleware +import com.tangem.tap.features.tokens.redux.TokensState import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware import org.rekotlin.Middleware import org.rekotlin.StateType @@ -24,7 +26,8 @@ data class AppState( val walletState: WalletState = WalletState(), val sendState: SendState = SendState(), val detailsState: DetailsState = DetailsState(), - val disclaimerState: DisclaimerState = DisclaimerState() + val disclaimerState: DisclaimerState = DisclaimerState(), + val tokensState: TokensState = TokensState(), ) : StateType { companion object { @@ -35,7 +38,8 @@ data class AppState( WalletMiddleware().walletMiddleware, sendMiddleware, DetailsMiddleware().detailsMiddleware, - DisclaimerMiddleware().disclaimerMiddleware + DisclaimerMiddleware().disclaimerMiddleware, + TokensMiddleware().tokensMiddleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index fc4f5bba9e..2a2a612757 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -1,22 +1,36 @@ package com.tangem.tap.common.redux.global -import com.tangem.tap.domain.config.ConfigManager +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 import com.tangem.tap.domain.tasks.ScanNoteResponse import com.tangem.tap.features.details.redux.SecurityOption +import com.tangem.tap.features.feedback.EmailData +import com.tangem.tap.features.feedback.FeedbackManager import org.rekotlin.Action -import java.math.BigDecimal sealed class GlobalAction : Action { + object ScanFailsCounter { + data class ChooseBehavior(val result: CompletionResult) : GlobalAction() + object Reset : GlobalAction() + object Increment : GlobalAction() + } + data class SaveScanNoteResponse(val scanNoteResponse: ScanNoteResponse) : GlobalAction() - data class SetFiatRate( - val fiatRates: Pair - ) : 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 SetConfigManager(val configManager: ConfigManager) : 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 SendFeedback(val emailData: EmailData) : GlobalAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index d7ec41960f..dce356f1be 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -1,6 +1,12 @@ 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.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 import com.tangem.tap.store import org.rekotlin.Middleware @@ -9,11 +15,44 @@ val globalMiddleware: Middleware = { 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() )) } + is GlobalAction.HideWarningMessage -> { + store.state.globalState.warningManager?.let { + if (it.hideWarning(action.warning)) { + if (WarningMessagesManager.isAlreadySignedHashesWarning(action.warning)) { + //TODO: No appropriate warningMessage identification. Make it better later + store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) + } + + store.dispatch(WalletAction.Warnings.Update) + store.dispatch(SendAction.Warnings.Update) + } + } + } + + is GlobalAction.SendFeedback -> { + store.state.globalState.feedbackManager?.send(action.emailData) + } } nextDispatch(action) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 7ab9334181..bfc866a06c 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -11,18 +11,19 @@ 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.SetFiatRate -> { - val rates = globalState.conversionRates.rates.toMutableMap() - rates[action.fiatRates.first] = action.fiatRates.second - globalState.copy(conversionRates = ConversionRates(rates)) - } is GlobalAction.ChangeAppCurrency -> { - globalState.copy(appCurrency = action.appCurrency, conversionRates = ConversionRates(mapOf())) + globalState.copy(appCurrency = action.appCurrency) } is GlobalAction.RestoreAppCurrency.Success -> { - globalState.copy(appCurrency = action.appCurrency, conversionRates = ConversionRates(mapOf())) + globalState.copy(appCurrency = action.appCurrency) } is GlobalAction.UpdateWalletSignedHashes -> { val card = globalState.scanNoteResponse?.card?.copy( @@ -37,6 +38,7 @@ fun globalReducer(action: Action, state: AppState): GlobalState { is GlobalAction.SetConfigManager -> { globalState.copy(configManager = action.configManager) } + is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager) is GlobalAction.UpdateSecurityOptions -> { val card = when (action.securityOption) { SecurityOption.LongTap -> globalState.scanNoteResponse?.card?.copy( @@ -55,6 +57,9 @@ fun globalReducer(action: Action, state: AppState): GlobalState { globalState } } + is GlobalAction.SetFeedbackManager -> { + globalState.copy(feedbackManager = action.feedbackManager) + } else -> globalState } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index f9e05cdeac..a0ac13bc06 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -4,11 +4,12 @@ import com.tangem.commands.common.network.TangemService import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY import com.tangem.tap.domain.PayIdManager import com.tangem.tap.domain.TapWalletManager -import com.tangem.tap.domain.config.ConfigManager +import com.tangem.tap.domain.configurable.config.ConfigManager +import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tasks.ScanNoteResponse +import com.tangem.tap.features.feedback.FeedbackManager import com.tangem.tap.network.coinmarketcap.CoinMarketCapService import org.rekotlin.StateType -import java.math.BigDecimal data class GlobalState( val scanNoteResponse: ScanNoteResponse? = null, @@ -16,20 +17,15 @@ data class GlobalState( val payIdManager: PayIdManager = PayIdManager(), val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(), val tangemService: TangemService = TangemService(), - val conversionRates: ConversionRates = ConversionRates(emptyMap()), val configManager: ConfigManager? = null, - val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY + val warningManager: WarningMessagesManager? = null, + val feedbackManager: FeedbackManager? = null, + val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY, + val scanCardFailsCounter: Int = 0, ) : StateType -data class ConversionRates( - val rates: Map, -) { - - fun getRate(cryptoCurrency: CryptoCurrencyName): BigDecimal? { - return rates[cryptoCurrency] - } -} - typealias CryptoCurrencyName = String typealias FiatCurrencyName = String + +interface StateDialog \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index 393a4e7e77..26344b0175 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -10,6 +10,6 @@ data class NavigationState( ) : StateType enum class AppScreen { - Home, Wallet, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer, - CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding + Home, Wallet, WalletDetails, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer, + CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding, AddTokens } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt b/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt index 3773bd41b7..a6e329ca04 100644 --- a/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt @@ -2,46 +2,13 @@ package com.tangem.tap.domain import com.tangem.blockchain.common.Blockchain import com.tangem.commands.common.network.Result -import com.tangem.tap.network.payid.PayIdService import com.tangem.tap.network.payid.PayIdVerifyService -import com.tangem.tap.network.payid.SetPayIdResponse import com.tangem.tap.network.payid.VerifyPayIdResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import retrofit2.HttpException import java.util.* class PayIdManager { - private val payIdService = PayIdService() - - suspend fun getPayId(cardId: String, publicKey: String): Result = withContext(Dispatchers.IO) { - val result = payIdService.getPayId(cardId, publicKey) - when (result) { - is Result.Success -> return@withContext Result.Success(result.data.payId) - is Result.Failure -> { - (result.error as? HttpException)?.let { - if (it.code() == 404) return@withContext Result.Success(null) - } - return@withContext result - } - } - } - - - suspend fun setPayId( - cardId: String, publicKey: String, payId: String, address: String, blockchain: Blockchain - ): Result = withContext(Dispatchers.IO) { - val result = payIdService.setPayId(cardId, publicKey, payId, address, blockchain.getPayIdNetwork()) - when (result) { - is Result.Success -> return@withContext result - is Result.Failure -> { - (result.error as? HttpException)?.let { - if (it.code() == 409) return@withContext Result.Failure(TapError.PayIdAlreadyCreated) - } - return@withContext result - } - } - } suspend fun verifyPayId(payId: String, blockchain: Blockchain): Result = withContext(Dispatchers.IO) { val splitPayId = payId.split("\$") diff --git a/app/src/main/java/com/tangem/tap/domain/TapBlockchainConfig.kt b/app/src/main/java/com/tangem/tap/domain/TapBlockchainConfig.kt new file mode 100644 index 0000000000..b9ec4705a5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/TapBlockchainConfig.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.domain + +class TapBlockchainConfig { + companion object { + val blockchairApiKey: String = "A___0Shpsu4KagE7oSabrw20DfXAqWlT" + val blockcypherTokens: Set = setOf( + "aa8184b0e0894b88a5688e01b3dc1e82", + "56c4ca23c6484c8f8864c32fde4def8d", + "66a8a37c5e9d4d2c9bb191acfe7f93aa" + ) + val infuraProjectId: String = "613a0b14833145968b1f656240c7d245" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index 37f159fdb8..521ed462b4 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -49,7 +49,7 @@ sealed class TapError( } sealed class TapSdkError(override val messageResId: Int?) : Throwable(), TangemError { - final override val code: Int = 1 + final override val code: Int = 50100 override var customMessage: String = code.toString() object CardForDifferentApp : TapSdkError(R.string.alert_unsupported_card) diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 36dd3c238c..497c7dde68 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -1,23 +1,23 @@ package com.tangem.tap.domain -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManager +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.common.extensions.toHexString import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.config.ConfigManager +import com.tangem.tap.currenciesRepository +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 import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.network.coinmarketcap.CoinMarketCapService @@ -28,29 +28,25 @@ import java.math.BigDecimal class TapWalletManager { - private val payIdManager = PayIdManager() private val coinMarketCapService = CoinMarketCapService() - suspend fun loadWalletData() { - val walletManager = store.state.globalState.scanNoteResponse?.walletManager - if (walletManager == null) { - store.dispatch(WalletAction.LoadWallet.Failure()) - return + private val blockchainSdkConfig by lazy { + store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig() + } + val walletManagerFactory: WalletManagerFactory + by lazy { WalletManagerFactory(blockchainSdkConfig) } + + suspend fun loadWalletData(walletManager: WalletManager) { + val result = try { + walletManager.update() + Result.Success(walletManager.wallet) + } catch (exception: Exception) { + Result.Failure(exception) } - loadWallet(walletManager) + handleUpdateWalletResult(result, walletManager) } - suspend fun loadPayId() { - val result = loadPayIdIfNeeded() - result?.let { handlePayIdResult(it) } - } - - suspend fun updateWallet() { - val walletManager = store.state.globalState.scanNoteResponse?.walletManager - if (walletManager == null) { - withContext(Dispatchers.Main) { store.dispatch(WalletAction.UpdateWallet.Failure()) } - return - } + suspend fun updateWallet(walletManager: WalletManager) { val result = try { walletManager.update() Result.Success(walletManager.wallet) @@ -68,12 +64,18 @@ class TapWalletManager { } - suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName) { - val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet ?: return - + suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) { val currencyList = wallet.getTokens().map { it.symbol }.toMutableList() currencyList.add(wallet.blockchain.currency) + loadFiatRate(fiatCurrency, currencyList) + } + suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, cryptoCurrencyName: CryptoCurrencyName) { + val currencyList = listOf(cryptoCurrencyName) + loadFiatRate(fiatCurrency, currencyList) + } + + private suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, currencyList: List) { val results = mutableListOf?>>() currencyList.forEach { results.add(it to coinMarketCapService.getRate(it, fiatCurrency)) } handleFiatRatesResult(results) @@ -84,24 +86,13 @@ class TapWalletManager { FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.CARD_IS_SCANNED, data.card) } TapWorkarounds.updateCard(data.card) - val configManager = store.state.globalState.configManager - if (TapWorkarounds.isStart2Coin) { - configManager?.turnOff(ConfigManager.isWalletPayIdEnabled) - configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled) - configManager?.turnOff(ConfigManager.isTopUpEnabled) - } else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin - || data.card.cardData?.blockchainName == Blockchain.Bitcoin.id){ - configManager?.turnOff(ConfigManager.isWalletPayIdEnabled) - configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) - configManager?.resetToDefault(ConfigManager.isTopUpEnabled) - } else { - configManager?.resetToDefault(ConfigManager.isWalletPayIdEnabled) - configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) - configManager?.resetToDefault(ConfigManager.isTopUpEnabled) - } + store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data.card) + updateConfigManager(data) + withContext(Dispatchers.Main) { store.dispatch(WalletAction.ResetState) store.dispatch(GlobalAction.SaveScanNoteResponse(data)) + store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed)) if (data.card.isTwinCard()) { val secondCardId = TwinsHelper.getTwinsCardId(data.card.cardId) val cardNumber = TwinsHelper.getTwinCardNumber(data.card.cardId) @@ -111,26 +102,55 @@ class TapWalletManager { )) } } + if (data.walletManager?.wallet?.blockchain == Blockchain.Ethereum || + data.walletManager?.wallet?.blockchain == Blockchain.EthereumTestnet) { + store.dispatch(TokensAction.LoadCardTokens) + } loadData(data) } } + private fun updateConfigManager(data: ScanNoteResponse) { + val configManager = store.state.globalState.configManager + if (TapWorkarounds.isStart2Coin) { + configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled) + configManager?.turnOff(ConfigManager.isTopUpEnabled) + } else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin + || data.card.cardData?.blockchainName == Blockchain.Bitcoin.id) { + configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) + configManager?.resetToDefault(ConfigManager.isTopUpEnabled) + } else { + configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) + configManager?.resetToDefault(ConfigManager.isTopUpEnabled) + } + } + suspend fun loadData(data: ScanNoteResponse) { withContext(Dispatchers.Main) { - store.dispatch(WalletAction.CheckIfWarningNeeded) 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 - store.dispatch(WalletAction.LoadWallet( - data.walletManager.wallet, data.verifyResponse?.artworkInfo?.id, config.isTopUpEnabled)) + val primaryWalletManager = data.walletManager + 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)) + if (primaryToken != null) { + store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken)) + } + if (data.card.isMultiwalletAllowed) { + loadMultiWalletData(data.card, primaryBlockchain, primaryWalletManager) + } else { + 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)) store.dispatch(WalletAction.LoadArtwork(data.card, artworkId)) - store.dispatch(WalletAction.LoadFiatRate) - store.dispatch(WalletAction.LoadPayId) + store.dispatch(WalletAction.LoadFiatRate()) } else if (data.card.status == CardStatus.Empty || data.card.isTwinCard()) { store.dispatch(WalletAction.EmptyWallet) store.dispatch(WalletAction.LoadArtwork(data.card, artworkId)) @@ -138,17 +158,54 @@ class TapWalletManager { store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain)) store.dispatch(WalletAction.LoadArtwork(data.card, artworkId)) } + store.dispatch(WalletAction.Warnings.CheckIfNeeded) } } - private suspend fun loadWallet(walletManager: WalletManager) { - val result = try { - walletManager.update() - Result.Success(walletManager.wallet) - } catch (exception: Exception) { - Result.Failure(exception) + 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) + } + } + + suspend fun reloadData(data: ScanNoteResponse) { + withContext(Dispatchers.Main) { + 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 + store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled)) + store.dispatch(WalletAction.LoadFiatRate()) + } else if (data.card.status == CardStatus.Empty || data.card.isTwinCard()) { + store.dispatch(WalletAction.EmptyWallet) + } else { + store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain)) + } } - handleUpdateWalletResult(result, walletManager) } private suspend fun handleUpdateWalletResult(result: Result, walletManager: WalletManager) { @@ -156,65 +213,34 @@ 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)) { val token = walletManager.wallet.getFirstToken() val amountToCreateAccount = blockchain.amountToCreateAccount(token) if (amountToCreateAccount != null) { - store.dispatch(WalletAction.LoadWallet.NoAccount(amountToCreateAccount.toString())) + store.dispatch(WalletAction.LoadWallet.NoAccount( + walletManager.wallet, + amountToCreateAccount.toString())) return@withContext } } - store.dispatch(WalletAction.LoadWallet.Failure(result.error?.localizedMessage)) + store.dispatch(WalletAction.LoadWallet.Failure(walletManager.wallet, result.error?.localizedMessage)) } } } } - - private suspend fun loadPayIdIfNeeded(): Result? { - val scanNoteResponse = store.state.globalState.scanNoteResponse - if (store.state.globalState.configManager?.config?.isWalletPayIdEnabled == false - || scanNoteResponse?.walletManager?.wallet?.blockchain?.isPayIdSupported() == false) { - return null - } - val cardId = scanNoteResponse?.card?.cardId - val publicKey = scanNoteResponse?.card?.cardPublicKey - if (cardId == null || publicKey == null) { - return null - } - return payIdManager.getPayId(cardId, publicKey.toHexString()) - } - - private suspend fun handlePayIdResult(result: Result) { - withContext(Dispatchers.Main) { - when (result) { - is Result.Success -> { - val config = store.state.globalState.configManager?.config - if (config?.isWalletPayIdEnabled == false) { - store.dispatch(WalletAction.DisablePayId) - return@withContext - } - val payId = result.data - if (payId == null) { - store.dispatch(WalletAction.LoadPayId.NotCreated) - } else { - store.dispatch(WalletAction.LoadPayId.Success(payId)) - } - } - is Result.Failure -> store.dispatch(WalletAction.LoadPayId.Failure) - } - } - } - private suspend fun handleFiatRatesResult(results: List?>>) { withContext(Dispatchers.Main) { results.map { when (it.second) { is Result.Success -> { val rate = it.first to (it.second as Result.Success).data - store.dispatch(GlobalAction.SetFiatRate(rate)) store.dispatch(WalletAction.LoadFiatRate.Success(rate)) } is Result.Failure -> store.dispatch(WalletAction.LoadFiatRate.Failure) diff --git a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt index d0cc8b64b2..48526204e8 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt @@ -1,6 +1,7 @@ package com.tangem.tap.domain import com.tangem.commands.common.card.Card +import com.tangem.commands.common.card.EllipticCurve import com.tangem.commands.common.card.masks.Product import java.util.* @@ -17,7 +18,7 @@ object TapWorkarounds { val cardData = this.cardData ?: return false val productMask = cardData.productMask val excludedBatch = excludedBatches.contains(cardData.batchId) - val excludedIssuerName = excludedIssuers.contains(cardData.issuerName?.capitalize(Locale.US)) + val excludedIssuerName = excludedIssuers.contains(cardData.issuerName?.toUpperCase(Locale.US)) val excludedProductMask = (productMask != null && // product mask is on cards v2.30 and later !productMask.contains(Product.Note) && !productMask.contains(Product.TwinCard)) return excludedBatch || excludedIssuerName || excludedProductMask @@ -35,4 +36,11 @@ object TapWorkarounds { private val excludedIssuers = listOf( "TTM BANK" ) -} \ No newline at end of file +} + +val Card.isMultiwalletAllowed: Boolean + get() { + return cardData?.productMask?.contains(Product.TwinCard) != true + && !TapWorkarounds.isStart2Coin + && this.curve == EllipticCurve.Secp256k1 + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/Loader.kt b/app/src/main/java/com/tangem/tap/domain/configurable/Loader.kt new file mode 100644 index 0000000000..3471ed78ee --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/configurable/Loader.kt @@ -0,0 +1,16 @@ +package com.tangem.tap.domain.configurable + +import com.tangem.wallet.BuildConfig + +/** +[REDACTED_AUTHOR] + */ +interface Loader { + fun load(onComplete: (T) -> Unit) + + companion object { + const val featuresName = "features_${BuildConfig.CONFIG_ENVIRONMENT}" + const val configValuesName = "tangem-android-config/config_${BuildConfig.CONFIG_ENVIRONMENT}" + const val warnings = "warnings_${BuildConfig.CONFIG_ENVIRONMENT}" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/config/ConfigLoader.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigLoaders.kt similarity index 58% rename from app/src/main/java/com/tangem/tap/domain/config/ConfigLoader.kt rename to app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigLoaders.kt index 2e90f4cb16..a47e8fec1c 100644 --- a/app/src/main/java/com/tangem/tap/domain/config/ConfigLoader.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigLoaders.kt @@ -1,52 +1,29 @@ -package com.tangem.tap.domain.config +package com.tangem.tap.domain.configurable.config import android.content.Context import com.google.firebase.ktx.Firebase import com.google.firebase.remoteconfig.ktx.remoteConfig -import com.google.firebase.remoteconfig.ktx.remoteConfigSettings import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler -import com.tangem.wallet.BuildConfig +import com.tangem.tap.domain.configurable.Loader import timber.log.Timber /** [REDACTED_AUTHOR] */ -interface ConfigLoader { - fun loadConfig(onComplete: (ConfigModel) -> Unit) - - companion object { - const val featuresName = "features_${BuildConfig.CONFIG_ENVIRONMENT}" - const val configValuesName = "config_${BuildConfig.CONFIG_ENVIRONMENT}" - - fun init() { - if (BuildConfig.CONFIG_ENVIRONMENT == "prod") { - Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings { - this.minimumFetchIntervalInSeconds = 1200 - }) - } else { - Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings { - this.minimumFetchIntervalInSeconds = 60 - }) - } - } - } -} - - -class LocalLoader( +class FeaturesLocalLoader( private val context: Context, private val moshi: Moshi, -) : ConfigLoader { +) : Loader { - override fun loadConfig(onComplete: (ConfigModel) -> Unit) { + override fun load(onComplete: (ConfigModel) -> Unit) { val config = try { val featureAdapter: JsonAdapter = moshi.adapter(FeatureModel::class.java) val valuesAdapter: JsonAdapter = moshi.adapter(ConfigValueModel::class.java) - val jsonFeatures = readAssetAsString(ConfigLoader.featuresName) - val jsonConfigValues = readAssetAsString(ConfigLoader.configValuesName) + val jsonFeatures = readAssetAsString(Loader.featuresName) + val jsonConfigValues = readAssetAsString(Loader.configValuesName) ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues)) } catch (ex: Exception) { @@ -61,16 +38,16 @@ class LocalLoader( } } -class RemoteLoader( +class FeaturesRemoteLoader( private val moshi: Moshi, -) : ConfigLoader { +) : Loader { - override fun loadConfig(onComplete: (ConfigModel) -> Unit) { + override fun load(onComplete: (ConfigModel) -> Unit) { val emptyConfig = ConfigModel.empty() val remoteConfig = Firebase.remoteConfig remoteConfig.fetchAndActivate().addOnCompleteListener { if (it.isSuccessful) { - val config = remoteConfig.getValue(ConfigLoader.featuresName) + val config = remoteConfig.getValue(Loader.featuresName) val jsonConfig = config.asString() if (jsonConfig.isEmpty()) { onComplete(emptyConfig) @@ -82,7 +59,7 @@ class RemoteLoader( onComplete(emptyConfig) } }.addOnFailureListener { - FirebaseAnalyticsHandler.logException("remote_config_error", it) + FirebaseAnalyticsHandler.logException("remote_config_error.features", it) onComplete(emptyConfig) } } diff --git a/app/src/main/java/com/tangem/tap/domain/config/ConfigManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt similarity index 73% rename from app/src/main/java/com/tangem/tap/domain/config/ConfigManager.kt rename to app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt index c7acd653c7..58096022fd 100644 --- a/app/src/main/java/com/tangem/tap/domain/config/ConfigManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt @@ -1,6 +1,8 @@ -package com.tangem.tap.domain.config +package com.tangem.tap.domain.configurable.config +import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.tangem_sdk_new.ui.animation.VoidCallback +import com.tangem.tap.domain.configurable.Loader /** [REDACTED_AUTHOR] @@ -9,15 +11,15 @@ data class Config( val coinMarketCapKey: String = "f6622117-c043-47a0-8975-9d673ce484de", val moonPayApiKey: String = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE", val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C", - val isWalletPayIdEnabled: Boolean = true, + val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(), val isSendingToPayIdEnabled: Boolean = true, val isTopUpEnabled: Boolean = false, val isCreatingTwinCardsAllowed: Boolean = false ) class ConfigManager( - private val localLoader: ConfigLoader, - private val remoteLoader: ConfigLoader + private val localLoader: Loader, + private val remoteLoader: Loader ) { var config: Config = Config() @@ -26,19 +28,19 @@ class ConfigManager( private var defaultConfig = Config() fun load(onComplete: VoidCallback? = null) { - localLoader.loadConfig { config -> + localLoader.load { config -> setupFeature(config.features) setupKey(config.configValues) - } - remoteLoader.loadConfig { config -> - setupFeature(config.features) onComplete?.invoke() } + // Uncomment to enable remote config +// remoteLoader.load { config -> +// setupFeature(config.features) +// } } fun turnOff(name: String) { when (name) { - isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = false) isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = false) isTopUpEnabled -> config = config.copy(isTopUpEnabled = false) isCreatingTwinCardsAllowed -> config = config.copy(isCreatingTwinCardsAllowed = false) @@ -47,7 +49,6 @@ class ConfigManager( fun resetToDefault(name: String) { when (name) { - isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = defaultConfig.isWalletPayIdEnabled) isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled) isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled) isCreatingTwinCardsAllowed -> config = @@ -59,13 +60,11 @@ class ConfigManager( val model = featureModel ?: return config = config.copy( - isWalletPayIdEnabled = model.isWalletPayIdEnabled, isTopUpEnabled = model.isTopUpEnabled, isSendingToPayIdEnabled = model.isSendingToPayIdEnabled, isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed ) defaultConfig = defaultConfig.copy( - isWalletPayIdEnabled = model.isWalletPayIdEnabled, isTopUpEnabled = model.isTopUpEnabled, isSendingToPayIdEnabled = model.isSendingToPayIdEnabled, isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed @@ -77,22 +76,28 @@ class ConfigManager( config = config.copy( coinMarketCapKey = values.coinMarketCapKey, moonPayApiKey = values.moonPayApiKey, - moonPayApiSecretKey = values.moonPayApiSecretKey + moonPayApiSecretKey = values.moonPayApiSecretKey, + blockchainSdkConfig = BlockchainSdkConfig( + blockchairApiKey = values.blockchairApiKey, + blockcypherTokens = values.blockcypherTokens, + infuraProjectId = values.infuraProjectId + ) ) defaultConfig = defaultConfig.copy( coinMarketCapKey = values.coinMarketCapKey, moonPayApiKey = values.moonPayApiKey, - moonPayApiSecretKey = values.moonPayApiSecretKey + moonPayApiSecretKey = values.moonPayApiSecretKey, + blockchainSdkConfig = BlockchainSdkConfig( + blockchairApiKey = values.blockchairApiKey, + blockcypherTokens = values.blockcypherTokens, + infuraProjectId = values.infuraProjectId + ) ) } companion object { - const val isWalletPayIdEnabled = "isWalletPayIdEnabled" const val isSendingToPayIdEnabled = "isSendingToPayIdEnabled" const val isCreatingTwinCardsAllowed = "isCreatingTwinCardsAllowed" const val isTopUpEnabled = "isTopUpEnabled" - const val coinMarketCapKey = "coinMarketCapKey" - const val moonPayApiKey = "moonPayApiKey" - const val moonPayApiSecretKey = "moonPayApiSecretKey" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/config/JsonModels.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt similarity index 75% rename from app/src/main/java/com/tangem/tap/domain/config/JsonModels.kt rename to app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt index 4724f8b2c3..69c5f0a8b0 100644 --- a/app/src/main/java/com/tangem/tap/domain/config/JsonModels.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.config +package com.tangem.tap.domain.configurable.config /** [REDACTED_AUTHOR] @@ -15,6 +15,9 @@ class ConfigValueModel( val coinMarketCapKey: String, val moonPayApiKey: String, val moonPayApiSecretKey: String, + val blockchairApiKey: String?, + val blockcypherTokens: Set?, + val infuraProjectId: String?, ) class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) { diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningLoader.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningLoader.kt new file mode 100644 index 0000000000..c95fc39a24 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningLoader.kt @@ -0,0 +1,52 @@ +package com.tangem.tap.domain.configurable.warningMessage + +import com.google.firebase.ktx.Firebase +import com.google.firebase.remoteconfig.ktx.remoteConfig +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler +import com.tangem.tap.domain.configurable.Loader + +/** +[REDACTED_AUTHOR] + */ +class RemoteWarningLoader( + private val moshi: Moshi, +) : Loader> { + + override fun load(onComplete: (List) -> Unit) { + val emptyConfig = listOf() + val remoteConfig = Firebase.remoteConfig + remoteConfig.fetchAndActivate().addOnCompleteListener { + if (!it.isSuccessful) { + onComplete(emptyConfig) + return@addOnCompleteListener + } + + val config = remoteConfig.getValue(Loader.warnings) + val jsonConfig = config.asString() + if (jsonConfig.isEmpty()) { + onComplete(emptyConfig) + return@addOnCompleteListener + } + + val adapterType = Types.newParameterizedType(List::class.java, WarningMessage::class.java) + val warningsAdapter: JsonAdapter> = moshi.adapter(adapterType) + try { + val warnings = warningsAdapter.fromJson(jsonConfig) ?: listOf() + onComplete(warnings) + } catch (ex: Exception) { + handleError(ex) + onComplete(emptyConfig) + } + }.addOnFailureListener { + handleError(it) + onComplete(emptyConfig) + } + } + + private fun handleError(ex: Exception) { + FirebaseAnalyticsHandler.logException("remote_config_error.warnings", ex) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessage.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessage.kt new file mode 100644 index 0000000000..ab94c0d563 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessage.kt @@ -0,0 +1,59 @@ +package com.tangem.tap.domain.configurable.warningMessage + +import com.squareup.moshi.Json +import com.tangem.blockchain.common.Blockchain + +/** +[REDACTED_AUTHOR] + */ +data class WarningMessage( + val title: String, + val message: String, + val type: Type, + val priority: Priority, + val location: List, + private val blockchains: List?, + val titleResId: Int? = null, + val messageResId: Int? = null, + val origin: Origin = Origin.Remote, +) { + val blockchainList: List? by lazy { + blockchains?.map { Blockchain.fromId(it.toUpperCase()) } + } + + + var isHidden = false + + enum class Priority { + @Json(name = "critical") + Critical, + + @Json(name = "warning") + Warning, + + @Json(name = "info") + Info + } + + enum class Type { + @Json(name = "permanent") + Permanent, // нельзя скрыть + + @Json(name = "temporary") + Temporary, // можно скрыть (кнопка ОК) + + AppRating + } + + enum class Location { + @Json(name = "main") + MainScreen, + + @Json(name = "send") + SendScreen + } + + enum class Origin { + Local, Remote + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt new file mode 100644 index 0000000000..81739ee7b5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt @@ -0,0 +1,129 @@ +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 + +/** +[REDACTED_AUTHOR] + */ +class WarningMessagesManager( + private val warningLoader: RemoteWarningLoader, +) { + + private val warningsList: MutableList = mutableListOf() + + fun load(onComplete: VoidCallback? = null) { + warningLoader.load { remoteList -> + warningsList.clear() + warningsList.addAll(remoteList) + sortByPriority() + onComplete?.invoke() + } + } + + fun addWarning(warning: WarningMessage) { + if (findWarning(warning) == null) { + warningsList.add(warning) + sortByPriority() + } + } + + fun getWarnings(location: WarningMessage.Location, forBlockchains: List = emptyList()): List { + return warningsList + .filter { !it.isHidden && it.location.contains(location) } + .filter { + val list = it.blockchainList + when { + list == null -> true + list.containsAny(forBlockchains) -> true + else -> false + } + } + } + + fun hideWarning(warning: WarningMessage): Boolean { + val foundWarning = findWarning(warning) + return when { + foundWarning == null -> false + foundWarning.type == WarningMessage.Type.Temporary + || foundWarning.type == WarningMessage.Type.AppRating -> { + if (foundWarning.isHidden) { + false + } else { + foundWarning.isHidden = true + true + } + } + else -> false + } + } + + fun removeWarnings(origin: WarningMessage.Origin) { + warningsList.removeIf { it.origin == origin } + sortByPriority() + } + + private fun sortByPriority() { + warningsList.sortBy { it.priority.ordinal } + } + + private fun findWarning(warning: WarningMessage): WarningMessage? { + return warningsList.firstOrNull { it == warning } + } + + companion object { + fun devCardWarning(): WarningMessage = WarningMessage( + "", + "", + type = WarningMessage.Type.Permanent, + priority = WarningMessage.Priority.Critical, + listOf(WarningMessage.Location.MainScreen), + null, + R.string.alert_title, + R.string.alert_developer_card, + WarningMessage.Origin.Local + ) + + fun alreadySignedHashesWarning(): WarningMessage = WarningMessage( + "", + "", + type = WarningMessage.Type.Temporary, + priority = WarningMessage.Priority.Info, + listOf(WarningMessage.Location.MainScreen), + null, + R.string.alert_title, + R.string.alert_card_signed_transactions, + WarningMessage.Origin.Local + ) + + fun appRatingWarning(): WarningMessage = WarningMessage( + "", + "", + WarningMessage.Type.AppRating, + WarningMessage.Priority.Info, + listOf(WarningMessage.Location.MainScreen), + null, + R.string.warning_rate_app_title, + R.string.warning_rate_app_message, + WarningMessage.Origin.Local + ) + + 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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt index c0e33bb929..3e24204388 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt @@ -1,9 +1,7 @@ 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 import com.tangem.commands.CommandResponse @@ -18,13 +16,14 @@ import com.tangem.tap.domain.TapSdkError import com.tangem.tap.domain.TapWorkarounds.isExcluded import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.domain.twins.isTwinCard +import com.tangem.tap.store import com.tangem.tasks.ScanTask data class ScanNoteResponse( val walletManager: WalletManager?, val card: Card, val verifyResponse: VerifyCardResponse? = null, - val secondTwinPublicKey: String? = null + val secondTwinPublicKey: String? = null, ) : CommandResponse class ScanNoteTask(val card: Card? = null) : CardSessionRunnable { @@ -53,7 +52,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable { callback(CompletionResult.Success(ScanNoteResponse( - walletManager, card, verifyResult.data, publicKey - ))) + walletManager, card, verifyResult.data, publicKey))) } is CompletionResult.Failure -> { callback(CompletionResult.Failure(TangemSdkError.CardVerificationFailed())) @@ -95,7 +93,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable> = moshi.adapter( + Types.newParameterizedType(List::class.java, TokenDao::class.java) + ) + private val blockchainsAdapter: JsonAdapter> = moshi.adapter( + Types.newParameterizedType(List::class.java, Blockchain::class.java) + ) + + 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) { + saveTokens(cardId, currencies.tokens) + saveBlockchains(cardId, currencies.blockchains) + } + + fun saveAddedToken(cardId: String, token: Token) { + val tokens = loadSavedTokens(cardId) + token + saveTokens(cardId, tokens) + } + + fun saveAddedBlockchain(cardId: String, blockchain: Blockchain) { + val blockchains = loadSavedBlockchains(cardId) + blockchain + saveBlockchains(cardId, blockchains) + } + + fun removeToken(cardId: String, token: Token) { + val tokens = loadSavedTokens(cardId).filterNot { it == token } + saveTokens(cardId, tokens) + } + + fun removeBlockchain(cardId: String, blockchain: Blockchain) { + val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain } + saveBlockchains(cardId, blockchains) + } + + private fun loadSavedTokens(cardId: String): List { + return try { + val json = context.readFileText(getFileNameForTokens(cardId)) + tokensAdapter.fromJson(json)!!.map { it.toToken() } + } catch (exception: Exception) { + emptyList() + } + } + + private fun saveTokens(cardId: String, tokens: List) { + val json = tokensAdapter.toJson(tokens.map { TokenDao.fromToken(it) }) + context.rewriteFile(json, getFileNameForTokens(cardId)) + } + + private fun loadSavedBlockchains(cardId: String): List { + return try { + val json = context.readFileText(getFileNameForBlockchains(cardId)) + blockchainsAdapter.fromJson(json) ?: emptyList() + } catch (exception: Exception) { + emptyList() + } + } + + private fun saveBlockchains(cardId: String, blockchains: List) { + val json = blockchainsAdapter.toJson(blockchains) + context.rewriteFile(json, getFileNameForBlockchains(cardId)) + } + + private fun Context.readFileText(fileName: String): String = + this.openFileInput(fileName).bufferedReader().readText() + + private fun Context.rewriteFile(content: String, fileName: String) { + this.openFileOutput(fileName, Context.MODE_PRIVATE).use { + it.write(content.toByteArray(), 0, content.length) + } + } + + fun getPopularTokens(): List { + val json = context.assets.readJsonFileToString(POPULAR_TOKENS_FILE_NAME) + return tokensAdapter.fromJson(json)!!.map { it.toToken() } + } + + fun getBlockchains(): List { + return listOf( + Blockchain.Bitcoin, Blockchain.BitcoinCash, Blockchain.Binance, Blockchain.Litecoin, + Blockchain.XRP, Blockchain.Tezos, + Blockchain.Ethereum, Blockchain.RSK) + } + + companion object { + private const val POPULAR_TOKENS_FILE_NAME = "erc20_tokens" + private const val FILE_NAME_PREFIX_TOKENS = "tokens" + private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains" + fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId" + fun getFileNameForBlockchains(cardId: String): String = "${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId" + } +} + + +@JsonClass(generateAdapter = true) +data class TokenDao( + val name: String, + val symbol: String, + val contractAddress: String, + val decimalCount: Int +) { + fun toToken(): Token { + return Token(name = name, symbol = symbol, contractAddress = contractAddress, decimals = decimalCount) + } + + companion object { + fun fromToken(token: Token): TokenDao { + return TokenDao(name = token.name, symbol = token.symbol, contractAddress = token.contractAddress, decimalCount = token.decimals) + + } + } +} + +@JsonClass(generateAdapter = true) +data class CardCurrenciesDao( + val tokens: List, + val blockchains: List +) { + fun toCardCurrencies(): CardCurrencies { + return CardCurrencies(tokens = tokens.map { it.toToken() }, blockchains = blockchains) + } + + companion object { + fun fromCardCurrencies(cardCurrencies: CardCurrencies): CardCurrenciesDao { + return CardCurrenciesDao(tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }, blockchains = cardCurrencies.blockchains) + } + } +} + +data class CardCurrencies( + val tokens: List, + val blockchains: List +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 65a2c7dff6..f37ef1c696 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -18,7 +18,7 @@ sealed class DetailsAction : Action { data class PrepareScreen( val card: Card, val scanNoteResponse: ScanNoteResponse, - val wallet: Wallet?, + val wallets: List, val isCreatingTwinWalletAllowed: Boolean?, val cardTou: CardTou, val fiatCurrencyName: FiatCurrencyName, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 95a0a3952c..b66aa958e9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -104,7 +104,7 @@ class DetailsMiddleware { is DetailsAction.AppCurrencyAction.SelectAppCurrency -> { preferencesStorage.saveAppCurrency(action.fiatCurrencyName) store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrencyName)) - store.dispatch(WalletAction.LoadFiatRate) + store.dispatch(WalletAction.LoadFiatRate()) } } } @@ -152,9 +152,7 @@ class DetailsMiddleware { store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure) } } - } - } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index ebb0485c03..ee44e36c9d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -66,8 +66,9 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta } else { null } + return DetailsState( - card = action.card, wallet = action.wallet, + card = action.card, wallets = action.wallets, cardInfo = action.card.toCardInfo(), appCurrencyState = AppCurrencyState( action.fiatCurrencyName @@ -82,8 +83,9 @@ private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsS return when (action) { DetailsAction.EraseWallet.Check -> { val notAllowedByCard = state.card?.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true - val notEmpty = state.wallet?.recentTransactions?.isNullOrEmpty() != true || - state.wallet.amounts.toSendableAmounts().isNotEmpty() + val notEmpty = state.wallets.any { + !it.recentTransactions.isNullOrEmpty() || it.amounts.toSendableAmounts().isNotEmpty() + } val eraseWalletState = when { notAllowedByCard -> EraseWalletState.NotAllowedByCard notEmpty -> EraseWalletState.NotEmpty diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 7995acdcdb..5ef1033e2f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -13,7 +13,7 @@ import java.util.* data class DetailsState( val card: Card? = null, - val wallet: Wallet? = null, + val wallets: List = emptyList(), val cardInfo: CardInfo? = null, val appCurrencyState: AppCurrencyState = AppCurrencyState(), val eraseWalletState: EraseWalletState? = null, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt index 8578a60304..895f379b09 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt @@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.twins.getTwinCardIdForUser import com.tangem.tap.domain.twins.isTwinCard @@ -15,6 +16,7 @@ import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.details.redux.twins.CreateTwinWallet +import com.tangem.tap.features.feedback.FeedbackEmail import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.fragment_details.* @@ -78,7 +80,6 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber + 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(targetedIntents.removeAt(0), "Send mail...") + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray()) + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + 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(activity.filesDir, "logs.txt") + file.delete() + file.createNewFile() + + val stringWriter = StringWriter() + logCollector.getLogs().forEach { stringWriter.append(it) } + val fileWriter = FileWriter(file) + fileWriter.write(stringWriter.toString()) + fileWriter.close() + logCollector.clearLogs() + file + } catch (ex: Exception) { + Timber.e(ex, "Can't create a file for email attachment") + null + } + } +} + +class TangemLogCollector : TangemSdkLogger { + private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS") + private val logs = mutableListOf() + + override fun log(message: () -> String, level: Log.Level) { + val time = dateFormatter.format(Date()) + logs.add("$time: ${message()}\n") + } + + fun getLogs(): List = logs.toList() + + fun clearLogs() { + logs.clear() + } +} + +class AdditionalEmailInfo { + class EmailWalletInfo( + var blockchain: Blockchain = Blockchain.Unknown, + var address: String = "", + var explorerLink: String = "", + // var outputsCount: String = "" + // var transactionHex: String = "" + ) + + var appVersion: String = "" + + // card + var cardId: String = "" + var cardFirmwareVersion: String = "" + + // wallets + internal val walletsInfo = mutableListOf() + 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 = "" + + fun setAppVersion(context: Context) { + try { + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) + appVersion = pInfo.versionName + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + } + } + + fun setCardInfo(card: Card) { + cardId = card.cardId + cardFirmwareVersion = card.firmwareVersion.version + signedHashesCount = card.walletSignedHashes?.toString() ?: "0" + } + + fun setWalletsInfo(wallets: List) { + 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 { + val subject: String + val mainMessage: String + fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String + + fun joinTogether(infoHolder: AdditionalEmailInfo): String { + return "$mainMessage\n\n\n\n\n" + + "Following information is optional. You can erase it if you don’t want to share it.\n" + + createOptionalMessage(infoHolder) + } +} + +class RateCanBeBetterEmail : EmailData { + override val subject: String = "My suggestions" + 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", walletInfo.blockchain.fullName) + appendKeyValue("Phone model", infoHolder.phoneModel) + appendKeyValue("OS version", infoHolder.osVersion) + appendKeyValue("App version", infoHolder.appVersion) + }.toString() + } +} + +class ScanFailsEmail : EmailData { + override val subject: String = "Can’t scan a card" + override val mainMessage: String = "Please tell us what card do you have?" + override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { + return StringBuilder().apply { + appendKeyValue("Phone model", infoHolder.phoneModel) + appendKeyValue("OS version", infoHolder.osVersion) + appendKeyValue("App version", infoHolder.appVersion) + }.toString() + } +} + +class SendTransactionFailedEmail(private val error: String) : EmailData { + override val subject: String = "Can’t 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", walletInfo.blockchain.fullName) + appendKeyValue("Token", infoHolder.token) + appendKeyValue("Source address", walletInfo.address) + appendKeyValue("Destination address", infoHolder.destinationAddress) + appendKeyValue("Amount", infoHolder.amount) + appendKeyValue("Fee", infoHolder.fee) + appendKeyValue("Phone model", infoHolder.phoneModel) + appendKeyValue("OS version", infoHolder.osVersion) + appendKeyValue("App version", infoHolder.appVersion) + appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion) +// appendKeyValue("Transaction HEX", infoHolder.transactionHex) + }.toString() + } +} + +class FeedbackEmail : EmailData { + override val subject: String = "Tangem Tap feedback" + override val mainMessage: String = "Hi Tangem," + override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { + 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) + builder.appendKeyValue("Phone model", infoHolder.phoneModel) + builder.appendKeyValue("OS version", infoHolder.osVersion) + return builder.toString() + } +} + +fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder { + return this.append("$key: $value\n") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index b8b8aa52fc..c93543613e 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -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 { + 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 { + if (dialog == null) dialog = ScanFailsDialog.create(requireContext()).apply { show() } + } + else -> { + dialog?.dismiss() + dialog = null + } + } } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index 5a9edf9fe7..4a575343c7 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -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() } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index aaf52f3564..b084b208cb 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt index b88da93641..3c031ed7b2 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt index b167e66785..c9fce9b492 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index bcb227de3b..08c3b35a09 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -3,9 +3,12 @@ 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 import com.tangem.tap.features.send.redux.states.MainCurrencyType import com.tangem.tap.features.send.redux.states.SendButtonState @@ -23,7 +26,10 @@ object ReleaseSendState : Action data class PrepareSendScreen( val coinAmount: Amount?, + val coinRate: BigDecimal?, + val walletManager: WalletManager?, val tokenAmount: Amount? = null, + val tokenRate: BigDecimal? = null ) : SendScreenAction // Address or PayId @@ -137,13 +143,19 @@ sealed class SendAction : SendScreenAction { data class SendError(override val error: TapError) : SendAction(), ErrorAction - sealed class Dialog : SendAction() { - data class ShowTezosWarningDialog( + 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() object Hide : Dialog() } + + sealed class Warnings : SendAction() { + object Update : SendAction() + data class Set(val warningList: List) : SendAction() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 2af762a25c..5a5f927b30 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -15,14 +15,18 @@ 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 import com.tangem.tap.features.send.redux.states.SendButtonState import com.tangem.tap.features.send.redux.states.TransactionExtrasState +import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.scope +import com.tangem.tap.store import com.tangem.tap.tangemSdk import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.rekotlin.Action @@ -43,6 +47,7 @@ val sendMiddleware: Middleware = { dispatch, appState -> is SendActionUi.SendAmountToRecipient -> verifyAndSendTransaction(action, appState(), dispatch) is PrepareSendScreen -> setIfSendingToPayIdEnabled(appState(), dispatch) + is SendAction.Warnings.Update -> updateWarnings(dispatch) } nextDispatch(action) } @@ -50,11 +55,11 @@ val sendMiddleware: Middleware = { dispatch, appState -> } private fun verifyAndSendTransaction( - action: SendActionUi.SendAmountToRecipient, appState: AppState?, dispatch: (Action) -> Unit + 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 @@ -66,7 +71,7 @@ private fun verifyAndSendTransaction( when { hadTezosError -> { val reduceAmount = walletManager.wallet.blockchain.minimalAmount() - dispatch(SendAction.Dialog.ShowTezosWarningDialog(reduceCallback = { + dispatch(SendAction.Dialog.TezosWarningDialog(reduceCallback = { dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false)) dispatch(AmountActionUi.CheckAmountToSend) }, sendAllCallback = { @@ -92,7 +97,7 @@ private fun sendTransaction( destinationAddress: String, transactionExtras: TransactionExtrasState, card: Card, - dispatch: (Action) -> Unit + dispatch: (Action) -> Unit, ) { dispatch(SendAction.ChangeSendButtonState(SendButtonState.PROGRESS)) var txData = walletManager.createTransaction(amountToSend, feeAmount, destinationAddress) @@ -116,6 +121,15 @@ private fun sendTransaction( dispatch(SendAction.SendSuccess) 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)) + } + } } is Result.Failure -> { when (result.error) { @@ -133,22 +147,27 @@ 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)) + infoHolder?.updateOnSendError(walletManager.wallet, amountToSend, feeAmount, destinationAddress) + dispatch(SendAction.Dialog.SendTransactionFails("unknown error")) } message.contains("50002") -> { // user was cancelled the operation by closing the Sdk bottom sheet } // make it easier latter by handling an appropriate enumError or, like on iOS, // accept a string identifier of the error message - message.contains("Target account is not created. To create account send 1+ XLM.")-> { + message.contains("Target account is not created. To create account send 1+ XLM.") -> { dispatch(SendAction.SendError(TapError.XmlError.AssetAccountNotCreated)) } else -> { Timber.e(throwable) FirebaseCrashlytics.getInstance().recordException(throwable) dispatch(SendAction.SendError(TapError.CustomError(message))) + infoHolder?.updateOnSendError(walletManager.wallet, amountToSend, feeAmount, destinationAddress) + dispatch(SendAction.Dialog.SendTransactionFails(message)) } } } @@ -207,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)) +} + diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt index 16da618931..9db3d6e7ca 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt @@ -42,8 +42,10 @@ private class SendReducer : SendInternalReducer { override fun handle(action: SendScreenAction, sendState: SendState): SendState { val result = when (action) { is SendAction.ChangeSendButtonState -> sendState.copy(sendButtonState = action.state) - is SendAction.Dialog.ShowTezosWarningDialog -> sendState.copy(dialog = action) + 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.Warnings.Set -> sendState.copy(sendWarningsList = action.warningList) else -> return sendState } @@ -58,16 +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 - val coinConverter = createCurrencyConverter(walletManager.wallet.blockchain.currency, decimals) - val tokenConverter = createCurrencyConverter(prepareAction.tokenAmount?.currencySymbol ?: "", decimals) return sendState.copy( walletManager = walletManager, - coinConverter = coinConverter, - tokenConverter = tokenConverter, + coinConverter = action.coinRate?.let { CurrencyConverter(it, decimals) }, + tokenConverter = action.tokenRate?.let { CurrencyConverter(it, decimals) }, amountState = sendState.amountState.copy( amountToExtract = amountToExtract, typeOfAmount = amountToExtract.type, @@ -78,11 +79,6 @@ private class PrepareSendScreenStatesReducer : SendInternalReducer { ) ) } - - private fun createCurrencyConverter(currency: String, decimals: Int): CurrencyConverter? { - val rate = store.state.globalState.conversionRates.getRate(currency) - return if (rate == null) null else CurrencyConverter(rate, decimals) - } } internal fun updateLastState(sendState: SendState, lastChangedState: IdStateHolder): SendState { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt index b85eb86bdc..1e4e8d5072 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt @@ -6,9 +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.features.send.redux.SendAction +import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.store import org.rekotlin.StateType import java.math.BigDecimal @@ -37,8 +38,9 @@ data class SendState( val amountState: AmountState = AmountState(), val feeState: FeeState = FeeState(), val receiptState: ReceiptState = ReceiptState(), + val sendWarningsList: List = listOf(), val sendButtonState: SendButtonState = SendButtonState.DISABLED, - val dialog: SendAction.Dialog? = null + val dialog: StateDialog? = null ) : SendScreenState { override val stateId: StateId = StateId.SEND_SCREEN diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 6dae105162..080114705d 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -9,9 +9,12 @@ import android.view.inputmethod.EditorInfo import android.widget.EditText import androidx.core.view.postDelayed import androidx.core.widget.addTextChangedListener +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.textfield.TextInputEditText import com.tangem.Message import com.tangem.merchant.common.toggleWidget.ToggleWidget +import com.tangem.tangem_sdk_new.extensions.dpToPx import com.tangem.tangem_sdk_new.extensions.hideSoftKeyboard import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.entities.TapCurrency @@ -30,10 +33,14 @@ import com.tangem.tap.features.send.redux.FeeActionUi.* import com.tangem.tap.features.send.redux.states.FeeType import com.tangem.tap.features.send.redux.states.MainCurrencyType import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber +import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration +import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.mainScope import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.fragment_send.* +import kotlinx.android.synthetic.main.fragment_send.rv_warning_messages +import kotlinx.android.synthetic.main.fragment_wallet.* import kotlinx.android.synthetic.main.layout_send_address_payid.* import kotlinx.android.synthetic.main.layout_send_amount.* import kotlinx.android.synthetic.main.layout_send_fee.* @@ -50,6 +57,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { lateinit var sendBtn: ToggleWidget private lateinit var etAmountToSend: TextInputEditText + private lateinit var warningsAdapter: WarningMessagesAdapter private fun initSendButtonStates() { sendBtn = ToggleWidget(flSendButtonContainer, btnSend, progress, ProgressState.None()) @@ -70,6 +78,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { setupTransactionExtrasLayout() setupAmountLayout() setupFeeLayout() + setupWarningMessages() btnSend.setOnClickListener { store.dispatch(SendActionUi.SendAmountToRecipient( @@ -232,6 +241,16 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { } } + private fun setupWarningMessages() { + warningsAdapter = WarningMessagesAdapter() + val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + rv_warning_messages.layoutManager = layoutManager + rv_warning_messages.addItemDecoration(SpacesItemDecoration(rv_warning_messages.dpToPx(16f).toInt())) + rv_warning_messages.adapter = warningsAdapter + + store.dispatch(SendAction.Warnings.Update) + } + override fun subscribeToStore() { store.subscribe(sendSubscriber) { appState -> appState.skipRepeats { oldState, newState -> diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt new file mode 100644 index 0000000000..cb65943200 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.send.ui.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.feedback.SendTransactionFailedEmail +import com.tangem.tap.features.send.redux.SendAction +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class SendTransactionFailsDialog { + + companion object { + fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(R.string.alert_failed_to_send_transaction_title) + setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, dialog.errorMessage)) + setNeutralButton(R.string.alert_button_send_feedback) { _, _ -> + store.dispatch(GlobalAction.SendFeedback(SendTransactionFailedEmail(dialog.errorMessage))) + } + setPositiveButton(R.string.common_no) { _, _ -> } + setOnDismissListener { store.dispatch(WalletAction.HideDialog) } + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/TezosWarningDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/TezosWarningDialog.kt index 45b092ea8c..afb6276cb7 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/TezosWarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/TezosWarningDialog.kt @@ -9,10 +9,10 @@ import com.tangem.wallet.R class TezosWarningDialog(context: Context) : AlertDialog(context) { companion object { - fun create(context: Context, showDialogData: SendAction.Dialog.ShowTezosWarningDialog): AlertDialog { + fun create(context: Context, showDialogData: SendAction.Dialog.TezosWarningDialog): AlertDialog { val reduceAmount = showDialogData.reduceAmount.toPlainString() return Builder(context).apply { - setTitle(context.getString(R.string.common_warning)) + setTitle(R.string.common_warning) setMessage(context.getString(R.string.xtz_withdrawal_message_warning, reduceAmount)) setNegativeButton(R.string.xtz_withdrawal_message_ignore) { _, _ -> showDialogData.sendAllCallback() diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index 3476f1ae19..973ed08f8b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -21,7 +21,9 @@ import com.tangem.tap.features.send.redux.reducers.ReceiptReducer import com.tangem.tap.features.send.redux.states.* import com.tangem.tap.features.send.ui.FeeUiHelper import com.tangem.tap.features.send.ui.SendFragment +import com.tangem.tap.features.send.ui.dialogs.SendTransactionFailsDialog import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog +import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.btn_expand_collapse.* @@ -96,12 +98,18 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber val sendFragment = (fg as? SendFragment) ?: return when (state.dialog) { - is SendAction.Dialog.ShowTezosWarningDialog -> { + is SendAction.Dialog.TezosWarningDialog -> { if (dialog == null) { dialog = TezosWarningDialog.create(fg.requireContext(), state.dialog) dialog?.show() } } + is SendAction.Dialog.SendTransactionFails -> { + if (dialog == null) { + dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog) + dialog?.show() + } + } else -> { dialog?.dismiss() dialog = null @@ -122,6 +130,12 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber sendFragment.sendBtn.setState(ProgressState.Progress(), true) } } + + val rv = fg.rv_warning_messages + val adapter = rv.adapter as? WarningMessagesAdapter ?: return + + adapter.submitList(state.sendWarningsList) + rv.show(state.sendWarningsList.isNotEmpty()) } private fun handleAddressPayIdState(fg: BaseStoreFragment, state: AddressPayIdState) { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt new file mode 100644 index 0000000000..e0c9abe119 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.features.tokens.redux + +import com.tangem.blockchain.common.Token +import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem +import com.tangem.tap.features.wallet.redux.WalletData +import org.rekotlin.Action + +sealed class TokensAction : Action { + + object LoadCurrencies : TokensAction() { + data class Success(val currencies: List) : TokensAction() + } + + object LoadCardTokens : TokensAction() { + data class Success(val tokens: List) : TokensAction() + } + + data class SetAddedCurrencies(val wallets: List) : TokensAction() + + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt new file mode 100644 index 0000000000..7923d17bd4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.features.tokens.redux + +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.currenciesRepository +import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem +import com.tangem.tap.store +import org.rekotlin.Middleware + +class TokensMiddleware { + + val tokensMiddleware: Middleware = { dispatch, state -> + { next -> + { action -> + when (action) { + is TokensAction.LoadCurrencies -> { + val tokens = currenciesRepository.getPopularTokens() + val blockchains = currenciesRepository.getBlockchains() + val currencies = CurrencyListItem.createListOfCurrencies( + blockchains, tokens + ) + store.dispatch(TokensAction.LoadCurrencies.Success(currencies)) + } + } + next(action) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt new file mode 100644 index 0000000000..e0c5600ad1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.tokens.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Action + +class TokensReducer { + companion object { + fun reduce(action: Action, state: AppState): TokensState = internalReduce(action, state) + } +} + +private fun internalReduce(action: Action, state: AppState): TokensState { + if (action !is TokensAction) return state.tokensState + + val tokensState = state.tokensState + return when (action) { + is TokensAction.LoadCurrencies.Success -> { + tokensState.copy(currencies = action.currencies) + } + is TokensAction.SetAddedCurrencies -> { + tokensState.copy(addedCurrencies = action.wallets.mapNotNull { it.currencyData.currencySymbol }) + } + is TokensAction.LoadCardTokens.Success -> { + tokensState.copy(addedTokens = LinkedHashSet( + action.tokens.map { TokenWithAmount(it, null) } + )) + } + else -> tokensState + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt new file mode 100644 index 0000000000..5169e63b96 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.features.tokens.redux + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Token +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem +import org.rekotlin.StateType + +data class TokensState( + val addedTokens: LinkedHashSet = LinkedHashSet(), + val addedCurrencies: List = emptyList(), + val currencies: List = emptyList(), +) : StateType + +data class TokenWithAmount( + val token: Token, + val amount: Amount? = null +) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt new file mode 100644 index 0000000000..e3d8eb80fb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt @@ -0,0 +1,119 @@ +package com.tangem.tap.features.tokens.ui + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.transition.TransitionInflater +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.tokens.redux.TokensState +import com.tangem.tap.features.tokens.ui.adapters.CurrenciesAdapter +import com.tangem.tap.mainScope +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.fragment_add_tokens.* +import kotlinx.android.synthetic.main.fragment_details_twin_cards.toolbar +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.* +import org.rekotlin.StoreSubscriber + + +class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), + StoreSubscriber { + + private lateinit var viewAdapter: CurrenciesAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + store.dispatch(NavigationAction.PopBackTo()) + } + }) + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(R.transition.slide_right) + exitTransition = inflater.inflateTransition(R.transition.fade) + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.tokensState == newState.tokensState + }.select { it.tokensState } + } + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + toolbar.setNavigationOnClickListener { activity?.onBackPressed() } + setupPopularTokensRecyclerView() + } + + + private fun setupPopularTokensRecyclerView() { + viewAdapter = CurrenciesAdapter() + rv_popular_tokens.layoutManager = LinearLayoutManager(context) + rv_popular_tokens.adapter = viewAdapter + } + + + override fun newState(state: TokensState) { + if (activity == null) return + viewAdapter.addedCurrencies = state.addedCurrencies + viewAdapter.submitUnfilteredList(state.currencies) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menu_search -> { + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.popular_tokens, menu) + + val menuItem = menu.findItem(R.id.menu_search) + val searchView: SearchView = menuItem.actionView as SearchView + searchView.queryHint = "Type here to search" + searchView.maxWidth = android.R.attr.width + searchView.inputtedTextAsFlow() + .debounce(400) + .distinctUntilChanged() + .onEach { + viewAdapter.filter(searchView.query) + } + .launchIn(mainScope) + return super.onCreateOptionsMenu(menu, inflater); + } +} + +fun SearchView.inputtedTextAsFlow(): Flow = callbackFlow { + val watcher = setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + offer(newText ?: "") + return false + } + }) + awaitClose { (watcher) } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt new file mode 100644 index 0000000000..9fe1ba05bd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt @@ -0,0 +1,165 @@ +package com.tangem.tap.features.tokens.ui.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.recyclerview.widget.DiffUtil +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.redux.global.CryptoCurrencyName +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.item_currency_subtitle.view.* +import kotlinx.android.synthetic.main.item_popular_token.view.* +import java.util.* + +class CurrenciesAdapter : ListAdapter(DiffUtilCallback) { + + var addedCurrencies: List = emptyList() + + private var unfilteredList = listOf() + + fun submitUnfilteredList(list: List) { + unfilteredList = list + submitList(list) + } + + override fun getItemViewType(position: Int): Int { + return when (currentList[position]) { + is CurrencyListItem.TitleListItem -> 0 + is CurrencyListItem.BlockchainListItem, is CurrencyListItem.TokenListItem -> 1 + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + 0 -> TitleViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_currency_subtitle, parent, false)) + 1 -> CurrenciesViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_popular_token, parent, false)) + else -> CurrenciesViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_popular_token, parent, false)) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val listItem = currentList[position] + if (holder is TitleViewHolder && listItem is CurrencyListItem.TitleListItem) { + holder.bind(listItem) + } else if (holder is CurrenciesViewHolder) { + holder.bind(listItem, addedCurrencies) + } + } + + object DiffUtilCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame( + oldItem: CurrencyListItem, newItem: CurrencyListItem + ) = oldItem == newItem + + override fun areItemsTheSame( + oldItem: CurrencyListItem, newItem: CurrencyListItem + ) = oldItem == newItem + } + + fun filter(query: CharSequence?) { + val list = mutableListOf() + + if (!query.isNullOrEmpty()) { + val queryNormalized = query.toString().toLowerCase(Locale.US) + list.addAll( + unfilteredList.filter { element -> + when (element) { + is CurrencyListItem.BlockchainListItem -> { + element.blockchain.currency.toLowerCase(Locale.US).contains(queryNormalized) + || element.blockchain.fullName.toLowerCase(Locale.US).contains(queryNormalized) + + } + is CurrencyListItem.TokenListItem -> { + element.token.name.toLowerCase(Locale.US).contains(queryNormalized) + || element.token.symbol.toLowerCase(Locale.US).contains(queryNormalized) + } + else -> false + } + }) + } else { + list.addAll(unfilteredList) + } + submitList(list) + } + + class CurrenciesViewHolder(val view: View) : + RecyclerView.ViewHolder(view) { + fun bind(currency: CurrencyListItem, addedCurrencies: List) { + when (currency) { + is CurrencyListItem.BlockchainListItem -> { + val blockchain = currency.blockchain + view.tv_currency_name.text = blockchain.fullName + view.tv_currency_symbol.text = blockchain.currency + val isAdded = addedCurrencies.any { it == blockchain.currency } + view.btn_add_token.show(!isAdded) + view.btn_token_added.show(isAdded) + + view.iv_currency.setImageResource(currency.blockchain.getIconRes()) + view.iv_currency.colorFilter = null + view.tv_token_letter.text = null + + view.btn_add_token.setOnClickListener { + store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchain)) + view.btn_add_token.hide() + view.btn_token_added.show() + } + } + is CurrencyListItem.TokenListItem -> { + val token = currency.token + view.tv_currency_name.text = token.name + view.tv_currency_symbol.text = token.symbol + + val isAdded = addedCurrencies.any { it == token.symbol } + view.btn_add_token.show(!isAdded) + view.btn_token_added.show(isAdded) + + view.iv_currency.setImageResource(R.drawable.shape_circle) + view.iv_currency.setColorFilter(token.getColor()) + view.tv_token_letter.text = token.symbol.take(1) + + view.btn_add_token.setOnClickListener { + store.dispatch(WalletAction.MultiWallet.AddToken(token)) + view.btn_add_token.hide() + view.btn_token_added.show() + } + } + } + } + } + + class TitleViewHolder(val view: View) : + RecyclerView.ViewHolder(view) { + fun bind(title: CurrencyListItem.TitleListItem) { + view.tv_subtitle.text = view.getString(title.titleResId).toUpperCase(Locale.US) + } + } +} + + +sealed class CurrencyListItem { + data class TokenListItem(val token: Token) : CurrencyListItem() + data class BlockchainListItem(val blockchain: Blockchain) : CurrencyListItem() + data class TitleListItem(@StringRes val titleResId: Int) : CurrencyListItem() + + companion object { + fun createListOfCurrencies(blockchains: List, tokens: List): List { + val blockchainsTitle = R.string.add_tokens_subtitle_blockchains + val tokensTitle = R.string.add_tokens_subtitle_tokens + return listOf(TitleListItem(blockchainsTitle)) + + blockchains.map { BlockchainListItem(it) } + + listOf(TitleListItem(tokensTitle)) + + tokens.map { TokenListItem(it) } + } + } +} + diff --git a/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt b/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt index 21cdc9d649..0ade915979 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.wallet.models +import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionStatus import com.tangem.tap.common.extensions.toFormattedString @@ -47,4 +48,13 @@ fun List.toPendingTransactions(walletAddress: String): List.removeUnknownTransactions(): List { return this.filter { it.type != PendingTransactionType.Unknown } +} + +fun TransactionData.toPendingTransactionForToken(token: Token, walletAddress: String): PendingTransaction? { + if (this.amount.currencySymbol != token.symbol) return null + return this.toPendingTransaction(walletAddress) +} + +fun List.toPendingTransactionsForToken(token: Token, walletAddress: String): List { + return this.mapNotNull { it.toPendingTransactionForToken(token, walletAddress) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index 0656a9583f..5b3877338d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -1,14 +1,15 @@ package com.tangem.tap.features.wallet.redux import android.content.Context -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.AddressType import com.tangem.commands.common.card.Card import com.tangem.tap.common.redux.ErrorAction 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 @@ -22,71 +23,100 @@ sealed class WalletAction : Action { data class Failure(val error: TapError) : WalletAction() } - data class LoadWallet( - val wallet: Wallet, val artworkId: String?, val allowTopUp: Boolean - ) : WalletAction() { + + data class LoadWallet(val allowTopUp: Boolean? = null, val currency: CryptoCurrencyName? = null) : WalletAction() { data class Success(val wallet: Wallet) : WalletAction() - data class NoAccount(val amountToCreateAccount: String) : WalletAction() - data class Failure(val errorMessage: String? = null) : WalletAction() + data class NoAccount(val wallet: Wallet, val amountToCreateAccount: String) : WalletAction() + data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction() } - object CheckIfWarningNeeded : WalletAction() - object CheckHashesCountOnline : WalletAction() - object NeedToCheckHashesCountOnline : WalletAction() - object ConfirmHashesCount : WalletAction() - data class ShowWarning(val warningType: WarningType) : WalletAction() - object SaveCardId : WalletAction() + data class SetArtworkId(val artworkId: String?) : WalletAction() - object UpdateWallet : WalletAction() { + + sealed class MultiWallet : WalletAction() { + data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet() + data class AddWalletManagers(val walletManagers: List) : MultiWallet() { + constructor(walletManager: WalletManager) : this(listOf(walletManager)) + } + data class AddBlockchain(val blockchain: Blockchain) : MultiWallet() + data class AddBlockchains(val blockchains: List) : MultiWallet() + data class AddTokens(val tokens: List) : 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() + data class SetPrimaryBlockchain(val blockchain: Blockchain) : MultiWallet() + data class SetPrimaryToken(val token: Token) : MultiWallet() + } + + sealed class Warnings : WalletAction() { + object CheckHashesCount : Warnings() { + object CheckHashesCountOnline : Warnings() + object NeedToCheckHashesCountOnline : Warnings() + object ConfirmHashesCount : Warnings() + object SaveCardId : Warnings() + } + + object CheckIfNeeded : Warnings() + object Update : Warnings() + data class Set(val warningList: List) : Warnings() + + object AppRating : Warnings() { + object SetNeverToShow : Warnings() + object RemindLater : Warnings() + } + } + + data class UpdateWallet(val currency: CryptoCurrencyName? = null) : WalletAction() { object ScheduleUpdatingWallet : WalletAction() data class Success(val wallet: Wallet) : WalletAction() data class Failure(val errorMessage: String? = null) : WalletAction() } - object LoadFiatRate : WalletAction() { - data class Success(val fiatRates: Pair) : WalletAction() + data class LoadFiatRate( + val wallet: Wallet? = null, val currency: CryptoCurrencyName? = null + ) : WalletAction() { + data class Success(val fiatRate: Pair) : WalletAction() object Failure : WalletAction() } - object LoadPayId : WalletAction() { - data class Success(val payId: String) : WalletAction() - object NotCreated : WalletAction() - object Failure : WalletAction() - } - - object DisablePayId : WalletAction() - data class LoadArtwork(val card: Card, val artworkId: String?) : WalletAction() { data class Success(val artwork: Artwork) : WalletAction() object Failure : WalletAction() } object Scan : WalletAction() + data class Send(val amount: Amount? = null) : WalletAction() { data class ChooseCurrency(val amounts: List?) : WalletAction() object Cancel : WalletAction() } - object CreatePayId : WalletAction() { - data class CompleteCreatingPayId(val payId: String) : WalletAction() - data class Success(val payId: String) : WalletAction() - object EmptyField : WalletAction(), ErrorAction { - override val error = TapError.PayIdEmptyField - } - class Failure(override val error: TapError) : WalletAction(), ErrorAction - object Cancel : WalletAction() + object EmptyField : WalletAction(), ErrorAction { + override val error = TapError.PayIdEmptyField } - data class CopyAddress(val context: Context) : WalletAction() { + data class CopyAddress(val address: String, val context: Context) : WalletAction() { object Success : WalletAction(), NotificationAction { override val messageResource = R.string.wallet_notification_address_copied } } - object ShowQrCode : WalletAction() + data class ShareAddress(val address: String, val context: Context) : WalletAction() + + object ShowDialog : WalletAction() { + object QrCode : WalletAction() + object ScanFails : WalletAction() + } + object HideDialog : WalletAction() - data class ExploreAddress(val context: Context) : WalletAction() + + data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction() + object CreateWallet : WalletAction() object EmptyWallet : WalletAction() @@ -101,7 +131,7 @@ sealed class WalletAction : Action { object SetOnboardingShown : TwinsAction() data class SetTwinCard( val secondCardId: String, val number: TwinCardNumber, - val isCreatingTwinCardsAllowed: Boolean + val isCreatingTwinCardsAllowed: Boolean, ) : TwinsAction() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt deleted file mode 100644 index 2f2c041924..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt +++ /dev/null @@ -1,304 +0,0 @@ -package com.tangem.tap.features.wallet.redux - -import android.content.Intent -import android.net.Uri -import androidx.browser.customtabs.CustomTabsIntent -import androidx.core.content.ContextCompat -import com.tangem.blockchain.common.* -import com.tangem.blockchain.extensions.SimpleResult -import com.tangem.commands.common.card.Card -import com.tangem.commands.common.card.CardType -import com.tangem.commands.common.network.Result -import com.tangem.common.CompletionResult -import com.tangem.common.extensions.getType -import com.tangem.common.extensions.toHexString -import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler -import com.tangem.tap.common.extensions.copyToClipboard -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.navigation.AppScreen -import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.PayIdManager -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.TopUpHelper -import com.tangem.tap.domain.extensions.toSendableAmounts -import com.tangem.tap.domain.twins.TwinsHelper -import com.tangem.tap.domain.twins.isTwinCard -import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.details.redux.twins.CreateTwinWallet -import com.tangem.tap.features.send.redux.PrepareSendScreen -import com.tangem.tap.features.wallet.models.toPendingTransactions -import com.tangem.tap.network.NetworkConnectivity -import com.tangem.tap.network.NetworkStateChanged -import com.tangem.tap.preferencesStorage -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.rekotlin.Action -import org.rekotlin.Middleware - -class WalletMiddleware { - private val topUpMiddleware = TopUpMiddleware() - - val walletMiddleware: Middleware = { dispatch, state -> - { next -> - { action -> - when (action) { - is WalletAction.TopUpAction -> topUpMiddleware.handle(action) - is WalletAction.LoadWallet -> { - scope.launch { - store.state.globalState.tapWalletManager.loadWalletData() - } - } - is WalletAction.LoadPayId -> { - scope.launch { - store.state.globalState.tapWalletManager.loadPayId() - } - } - is WalletAction.LoadFiatRate -> { - scope.launch { - store.state.globalState.tapWalletManager.loadFiatRate(store.state.globalState.appCurrency) - } - } - is WalletAction.CreateWallet -> { - if (store.state.walletState.twinCardsState != null) { - store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning( - store.state.globalState.scanNoteResponse?.card?.cardId?.let { - TwinsHelper.getTwinCardNumber(it) - }, - CreateTwinWallet.CreateWallet - )) - } else { - scope.launch { - val result = tangemSdkManager.createWallet( - store.state.globalState.scanNoteResponse?.card?.cardId - ) - when (result) { - is CompletionResult.Success -> { - store.state.globalState.tapWalletManager - .onCardScanned(result.data) - } - - } - } - } - } - is WalletAction.UpdateWallet -> { - if (store.state.walletState.state == ProgressState.Done) { - scope.launch { store.state.globalState.tapWalletManager.updateWallet() } - } - } - is WalletAction.UpdateWallet.Success -> setupWalletUpdate(action.wallet) - is WalletAction.LoadWallet.Success -> { - store.dispatch(WalletAction.CheckHashesCountOnline) - if (!store.state.walletState.updatingWallet) setupWalletUpdate(action.wallet) - } - is WalletAction.CreatePayId.CompleteCreatingPayId -> { - scope.launch { - val cardId = store.state.globalState.scanNoteResponse?.card?.cardId - val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet - val publicKey = store.state.globalState.scanNoteResponse?.card?.cardPublicKey - if (cardId != null && wallet != null && publicKey != null) { - val result = PayIdManager().setPayId( - cardId, publicKey.toHexString(), - action.payId, wallet.address, wallet.blockchain - ) - withContext(Dispatchers.Main) { - when (result) { - is Result.Success -> - store.dispatch(WalletAction.CreatePayId.Success(action.payId)) - is Result.Failure -> { - val error = result.error as? TapError - ?: TapError.PayIdCreatingError - store.dispatch(WalletAction.CreatePayId.Failure(error)) - } - } - } - } - } - } - is WalletAction.Scan -> { - scope.launch { - val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler) - when (result) { - is CompletionResult.Success -> { - tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card) - store.state.globalState.tapWalletManager - .onCardScanned(result.data, true) - if (store.state.walletState.twinCardsState != null) { - val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown() - if (showOnboarding) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding)) - } - } - } - } - } - } - is WalletAction.LoadData -> { - scope.launch { - store.state.globalState.scanNoteResponse?.let { - store.state.globalState.tapWalletManager.loadData(it) - } - } - } - is NetworkStateChanged -> { - store.state.globalState.scanNoteResponse?.let { scanNoteResponse -> - store.dispatch(WalletAction.CheckHashesCountOnline) - scope.launch { - store.state.globalState.tapWalletManager.onCardScanned(scanNoteResponse) - } - } - } - is WalletAction.CopyAddress -> { - store.state.walletState.walletAddresses?.selectedAddress?.address?.let { - action.context.copyToClipboard(it) - store.dispatch(WalletAction.CopyAddress.Success) - } - } - is WalletAction.ExploreAddress -> { - val uri = Uri.parse(store.state.walletState.walletAddresses?.selectedAddress?.exploreUrl) - val intent = Intent(Intent.ACTION_VIEW, uri) - ContextCompat.startActivity(action.context, intent, null) - } - is WalletAction.Send -> { - val newAction = prepareSendAction(action.amount) - store.dispatch(newAction) - if (newAction is PrepareSendScreen) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.Send)) - } - } - is WalletAction.CheckIfWarningNeeded -> { - val card = store.state.globalState.scanNoteResponse?.card - val validator = store.state.globalState.scanNoteResponse?.walletManager - as? SignatureCountValidator - if (card != null && !preferencesStorage.wasCardScannedBefore(card.cardId)) { - val result = checkIfWarningNeeded(card, validator) - if (result != null) store.dispatch(WalletAction.ShowWarning(result)) - } - } - is WalletAction.CheckHashesCountOnline -> checkHashesCountOnline() - is WalletAction.SaveCardId -> { - val cardId = store.state.globalState.scanNoteResponse?.card?.cardId - cardId?.let { preferencesStorage.saveScannedCardId(it) } - } - is WalletAction.TwinsAction.SetTwinCard -> { - val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown() - if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding) - } - is WalletAction.TwinsAction.SetOnboardingShown -> { - preferencesStorage.saveTwinsOnboardingShown() - } - } - next(action) - } - } - } - - private fun setupWalletUpdate(wallet: Wallet) { - if (!wallet.recentTransactions.toPendingTransactions(wallet.address).isNullOrEmpty()) { - store.dispatch(WalletAction.UpdateWallet.ScheduleUpdatingWallet) - scope.launch(Dispatchers.IO) { - delay(10000) - withContext(Dispatchers.Main) { - store.dispatch(WalletAction.UpdateWallet) - } - } - } - } - - - private fun prepareSendAction(amount: Amount?): Action { - return if (amount != null) { - if (amount.type is AmountType.Token) { - PrepareSendScreen(store.state.walletState.wallet?.amounts?.get(AmountType.Coin), amount) - } else { - PrepareSendScreen(amount) - } - } else { - val amounts = store.state.walletState.wallet?.amounts?.toSendableAmounts() - if (amounts?.size ?: 0 > 1) { - WalletAction.Send.ChooseCurrency(amounts) - } else { - val amountToSend = amounts?.first() - PrepareSendScreen(amountToSend) - } - } - } - - private fun checkIfWarningNeeded( - card: Card, signatureCountValidator: SignatureCountValidator? = null - ): WarningType? { - - if (card.getType() != CardType.Release) { - return WarningType.DevCard - } - if (card.isTwinCard()) return null - - return if (signatureCountValidator == null) { - if (card.walletSignedHashes ?: 0 > 0) { - WarningType.CardSignedHashesBefore - } else { - store.dispatch(WalletAction.SaveCardId) - null - } - } else { - store.dispatch(WalletAction.NeedToCheckHashesCountOnline) - null - } - } - - private fun checkHashesCountOnline() { - if (store.state.walletState.hashesCountVerified != false) return - if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return - - val card = store.state.globalState.scanNoteResponse?.card - if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return - - if (card.isTwinCard()) return - - val validator = store.state.globalState.scanNoteResponse?.walletManager - as? SignatureCountValidator - scope.launch { - val result = validator?.validateSignatureCount(card.walletSignedHashes ?: 0) - withContext(Dispatchers.Main) { - when (result) { - SimpleResult.Success -> { - store.dispatch(WalletAction.ConfirmHashesCount) - store.dispatch(WalletAction.SaveCardId) - } - is SimpleResult.Failure -> - if (result.error is BlockchainSdkError.SignatureCountNotMatched) { - store.dispatch(WalletAction.ShowWarning(WarningType.CardSignedHashesBefore)) - } else if (card.walletSignedHashes ?: 0 > 0) { - store.dispatch(WalletAction.ShowWarning(WarningType.CardSignedHashesBefore)) - } - } - } - } - } -} - -private class TopUpMiddleware { - fun handle(action: WalletAction.TopUpAction) { - when (action) { - is WalletAction.TopUpAction.TopUp -> { - val config = store.state.globalState.configManager?.config ?: return - val defaultAddress = store.state.walletState.walletAddresses!!.list[0].address - val url = TopUpHelper.getUrl( - store.state.walletState.currencyData.currencySymbol!!, - defaultAddress, - config.moonPayApiKey, - config.moonPayApiSecretKey - ) - val customTabsIntent = CustomTabsIntent.Builder() - .setToolbarColor(action.toolbarColor) - .build() - customTabsIntent.launchUrl(action.context, Uri.parse(url)); - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt deleted file mode 100644 index 533121bbe8..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt +++ /dev/null @@ -1,337 +0,0 @@ -package com.tangem.tap.features.wallet.redux - -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.Wallet -import com.tangem.commands.common.network.TangemService -import com.tangem.common.extensions.isZero -import com.tangem.common.extensions.toHexString -import com.tangem.tap.common.extensions.toFiatString -import com.tangem.tap.common.extensions.toFormattedCurrencyString -import com.tangem.tap.common.extensions.toFormattedString -import com.tangem.tap.common.extensions.toQrCode -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.getFirstToken -import com.tangem.tap.domain.twins.TwinCardNumber -import com.tangem.tap.features.wallet.models.removeUnknownTransactions -import com.tangem.tap.features.wallet.models.toPendingTransactions -import com.tangem.tap.features.wallet.ui.BalanceStatus -import com.tangem.tap.features.wallet.ui.BalanceWidgetData -import com.tangem.tap.features.wallet.ui.TokenData -import com.tangem.tap.store -import org.rekotlin.Action - -class WalletReducer { - companion object { - fun reduce(action: Action, state: AppState): WalletState = internalReduce(action, state) - } -} - -private fun internalReduce(action: Action, state: AppState): WalletState { - - if (action !is WalletAction) return state.walletState - - var newState = state.walletState - - when (action) { - is WalletAction.ResetState -> newState = WalletState() - is WalletAction.EmptyWallet -> { - val creatingWalletAllowed = !(newState.twinCardsState != null && - newState.twinCardsState?.isCreatingTwinCardsAllowed != true) - - newState = newState.copy( - state = ProgressState.Done, - currencyData = BalanceWidgetData(BalanceStatus.EmptyCard), - mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed), - topUpState = TopUpState(false) - ) - } - is WalletAction.LoadData.Failure -> { - when (action.error) { - is TapError.NoInternetConnection -> { - val wallet = state.globalState.scanNoteResponse?.walletManager?.wallet - newState = newState.copy( - state = ProgressState.Error, - error = ErrorType.NoInternetConnection, - walletAddresses = createAddressList(wallet, newState.walletAddresses), - currencyData = BalanceWidgetData( - status = BalanceStatus.Unreachable, - currency = wallet?.blockchain?.fullName, - token = wallet?.getFirstToken()?.symbol?.let { - TokenData("", tokenSymbol = it) - }), - mainButton = WalletMainButton.SendButton(false), - topUpState = TopUpState(false) - ) - } - is TapError.UnknownBlockchain -> { - newState = newState.copy( - state = ProgressState.Done, - currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain), - topUpState = TopUpState(false) - ) - } - } - - } - is WalletAction.LoadWallet -> { - val wallet = action.wallet - val cardImage = if (newState.cardImage?.artworkId == action.artworkId) { - newState.cardImage - } else { - null - } - newState = newState.copy( - state = ProgressState.Loading, - cardImage = cardImage, - currencyData = BalanceWidgetData( - BalanceStatus.Loading, - wallet.blockchain.fullName, - currencySymbol = wallet.blockchain.currency, - token = wallet.getFirstToken()?.symbol?.let { - TokenData("", tokenSymbol = it) - } - ), - walletAddresses = createAddressList(wallet, newState.walletAddresses), - mainButton = WalletMainButton.SendButton(false), - topUpState = TopUpState(allowed = action.allowTopUp) - ) - } - is WalletAction.LoadWallet.Success -> newState = onWalletLoaded(action.wallet, newState) - - is WalletAction.LoadWallet.NoAccount -> { - val wallet = state.globalState.scanNoteResponse?.walletManager?.wallet - newState = newState.copy( - state = ProgressState.Done, wallet = wallet, - currencyData = BalanceWidgetData( - BalanceStatus.NoAccount, wallet?.blockchain?.fullName, - currencySymbol = wallet?.blockchain?.currency, - amountToCreateAccount = action.amountToCreateAccount - ) - ) - } - is WalletAction.LoadWallet.Failure -> newState = newState.copy( - state = ProgressState.Done, - currencyData = newState.currencyData.copy( - status = BalanceStatus.Unreachable, - errorMessage = action.errorMessage - ), - topUpState = TopUpState(false) - ) - 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.Success -> { - newState = onWalletLoaded(action.wallet, newState) - newState = newState.copy(updatingWallet = newState.pendingTransactions.isNotEmpty()) - } - 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 -> { - val rate = action.fiatRates.second ?: return newState - val currency = action.fiatRates.first - val fiatAmount = if (currency == newState.wallet?.blockchain?.currency) { - newState.wallet?.amounts?.get(AmountType.Coin)?.value - ?.toFiatString(rate, state.globalState.appCurrency) - } else { - newState.currencyData.fiatAmount - } - val token = newState.wallet?.getFirstToken() - val tokenFiatAmount = if (currency == token?.symbol) { - newState.wallet?.getTokenAmount(token)?.value?.toFiatString(rate, state.globalState.appCurrency) - } else { - newState.currencyData.token?.fiatAmount - } - newState = newState.copy(currencyData = newState.currencyData.copy( - fiatAmount = fiatAmount, - token = newState.currencyData.token?.copy(fiatAmount = tokenFiatAmount) - )) - } - is WalletAction.LoadArtwork -> { - val cardId = action.card.cardId - val cardPublicKey = action.card.cardPublicKey?.toHexString() - val artworkUrl = if (cardPublicKey != null && action.artworkId != null) { - TangemService.getUrlForArtwork(cardId, cardPublicKey, action.artworkId) - } else if (action.card.cardId.startsWith(Artwork.SERGIO_CARD_ID)) { - Artwork.SERGIO_CARD_URL - } else if (action.card.cardId.startsWith(Artwork.MARTA_CARD_ID)) { - Artwork.MARTA_CARD_URL - } else if (newState.twinCardsState?.cardNumber != null) { - when (newState.twinCardsState?.cardNumber) { - TwinCardNumber.First -> Artwork.TWIN_CARD_1 - TwinCardNumber.Second -> Artwork.TWIN_CARD_2 - null -> Artwork.DEFAULT_IMG_URL - } - } else { - Artwork.DEFAULT_IMG_URL - } - newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl)) - } - is WalletAction.ShowQrCode -> { - newState = newState.copy( - walletDialog = WalletDialog.QrDialog( - newState.walletAddresses?.selectedAddress?.shareUrl?.toQrCode(), - newState.walletAddresses?.selectedAddress?.shareUrl, - newState.currencyData.currency - ) - ) - } - is WalletAction.HideDialog -> { - newState = newState.copy(walletDialog = null) - } - is WalletAction.LoadPayId.Success -> newState = newState.copy( - payIdData = PayIdData(PayIdState.Created, action.payId) - ) - is WalletAction.LoadPayId.NotCreated -> newState = newState.copy( - payIdData = PayIdData(PayIdState.NotCreated, null) - ) - is WalletAction.DisablePayId -> newState = newState.copy( - payIdData = PayIdData(PayIdState.Disabled, null) - ) - is WalletAction.CreatePayId, is WalletAction.CreatePayId.Failure -> - newState = newState.copy( - walletDialog = WalletDialog.CreatePayIdDialog(CreatingPayIdState.EnterPayId) - ) - is WalletAction.CreatePayId.CompleteCreatingPayId -> newState = newState.copy( - walletDialog = WalletDialog.CreatePayIdDialog(CreatingPayIdState.Waiting) - ) - is WalletAction.CreatePayId.Success -> newState = newState.copy( - payIdData = PayIdData(PayIdState.Created, action.payId), - walletDialog = null - ) - is WalletAction.CreatePayId.Cancel -> newState = newState.copy(walletDialog = null) - is WalletAction.Send.ChooseCurrency -> { - newState = newState.copy( - walletDialog = WalletDialog.SelectAmountToSendDialog(action.amounts) - ) - } - is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null) - is WalletAction.ShowWarning -> - newState = newState.copy(walletDialog = WalletDialog.WarningDialog(action.warningType)) - is WalletAction.NeedToCheckHashesCountOnline -> - newState = newState.copy(hashesCountVerified = false) - is WalletAction.ConfirmHashesCount -> - newState = newState.copy(hashesCountVerified = true) - is WalletAction.TopUpAction -> { - newState = newState.copy(topUpState = handleTopUpActions(action, newState.topUpState)) - } - is WalletAction.ChangeSelectedAddress -> { - val walletAddresses = newState.walletAddresses ?: return newState - val address = walletAddresses.list.firstOrNull { it.type == action.type } - ?: return newState - - newState = newState.copy(walletAddresses = WalletAddresses(address, walletAddresses.list)) - } - is WalletAction.TwinsAction.SetTwinCard -> { - newState = newState.copy( - twinCardsState = TwinCardsState( - secondCardId = action.secondCardId, - cardNumber = action.number, - showTwinOnboarding = newState.twinCardsState?.showTwinOnboarding - ?: false, - isCreatingTwinCardsAllowed = action.isCreatingTwinCardsAllowed - ) - ) - } - is WalletAction.TwinsAction.ShowOnboarding -> { - newState = newState.copy( - twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = true) - ?: TwinCardsState(null, null, - showTwinOnboarding = true, - isCreatingTwinCardsAllowed = false) - ) - } - is WalletAction.TwinsAction.SetOnboardingShown -> { - newState = newState.copy( - twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = false) - ) - } - } - return newState -} - -fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null): WalletAddresses? { - if (wallet == null) return null - - val listOfAddressData = mutableListOf() - // put a defaultAddress at the first place - wallet.addresses.forEach { - val addressData = AddressData(it.value, it.type, wallet.getShareUri(it.value), wallet.getExploreUrl(it.value)) - if (it.type == wallet.blockchain.defaultAddressType()) { - listOfAddressData.add(0, addressData) - } else { - listOfAddressData.add(addressData) - } - } - - // restore a selected wallet address - var indexOfSelectedWallet = 0 - walletAddresses?.let { - val index = listOfAddressData.indexOfFirst { it.address == walletAddresses.selectedAddress.address } - if (index != -1) indexOfSelectedWallet = index - } - return WalletAddresses(listOfAddressData[indexOfSelectedWallet], listOfAddressData) -} - -private fun handleTopUpActions(action: WalletAction.TopUpAction, state: TopUpState): TopUpState { - return when (action) { - is WalletAction.TopUpAction.TopUp -> state - } -} - -private fun onWalletLoaded( - wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null -): WalletState { - val fiatCurrencySymbol = store.state.globalState.appCurrency - val token = wallet.getFirstToken() - val tokenData = if (token != null) { - val tokenAmount = wallet.getTokenAmount(token) - if (tokenAmount != null) { - val tokenFiatRate = store.state.globalState.conversionRates.getRate(tokenAmount.currencySymbol) - val tokenFiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencySymbol) } - TokenData(tokenAmount.value?.toFormattedString(tokenAmount.decimals) ?: "", - tokenAmount.currencySymbol, tokenFiatAmount) - } else { - null - } - } else { - null - } - val amount = wallet.amounts[AmountType.Coin]?.value - val formattedAmount = amount?.toFormattedCurrencyString( - wallet.blockchain.decimals(), - wallet.blockchain.currency) - val fiatRate = store.state.globalState.conversionRates.getRate(wallet.blockchain.currency) - val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) } - - val pendingTransactions = wallet.recentTransactions - .toPendingTransactions(wallet.address) - - val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty() - val balanceStatus = if (pendingTransactions.isNotEmpty()) { - BalanceStatus.TransactionInProgress - } else { - BalanceStatus.VerifiedOnline - } - return walletState.copy( - state = ProgressState.Done, wallet = wallet, - currencyData = BalanceWidgetData( - balanceStatus, wallet.blockchain.fullName, - currencySymbol = wallet.blockchain.currency, - formattedAmount, - token = tokenData, - fiatAmount = fiatAmount - ), - pendingTransactions = pendingTransactions.removeUnknownTransactions(), - mainButton = WalletMainButton.SendButton(sendButtonEnabled) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index ec4c134bab..244f7f026d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -1,74 +1,124 @@ package com.tangem.tap.features.wallet.redux import android.graphics.Bitmap -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.* 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 import com.tangem.tap.features.wallet.models.PendingTransaction +import com.tangem.tap.features.wallet.models.toPendingTransactions +import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken import com.tangem.tap.features.wallet.ui.BalanceWidgetData +import com.tangem.tap.store import org.rekotlin.StateType +import java.math.BigDecimal data class WalletState( val state: ProgressState = ProgressState.Done, val error: ErrorType? = null, val cardImage: Artwork? = null, - val wallet: Wallet? = null, - val pendingTransactions: List = emptyList(), val hashesCountVerified: Boolean? = null, - val walletAddresses: WalletAddresses? = null, - val currencyData: BalanceWidgetData = BalanceWidgetData(), - val payIdData: PayIdData = PayIdData(), - val walletDialog: WalletDialog? = null, - val updatingWallet: Boolean = false, - val mainButton: WalletMainButton = WalletMainButton.SendButton(false), - val topUpState: TopUpState = TopUpState(), + val walletDialog: StateDialog? = null, val twinCardsState: TwinCardsState? = null, + val mainWarningsList: List = mutableListOf(), + val wallets: List = emptyList(), + val walletManagers: List = emptyList(), + val isMultiwalletAllowed: Boolean = false, + val cardCurrency: CryptoCurrencyName? = null, + val selectedWallet: CryptoCurrencyName? = null, + val primaryBlockchain: Blockchain? = null, + val primaryToken: Token? = null ) : StateType { - val showDetails: Boolean = - currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard && - currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.UnknownBlockchain - val showMultipleAddress: Boolean - get() { - val listOfAddresses = walletAddresses?.list ?: return false - return (wallet?.blockchain == Blockchain.Bitcoin || - wallet?.blockchain == Blockchain.BitcoinTestnet || - wallet?.blockchain == Blockchain.CardanoShelley) && - listOfAddresses.size > 1 + val primaryWallet = if (wallets.isNotEmpty()) wallets[0] else null +// val primaryWalletManager = if (walletManagers.isNotEmpty()) walletManagers[0] else null + + val shouldShowDetails: Boolean = + 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 + 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 } + 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 { + val primaryWalletManager = walletManagers.find { it.wallet.blockchain == primaryBlockchain } + if (primaryWalletManager?.presetTokens?.find { it.symbol == currencyName } != null) { + primaryWalletManager + } else { + ethereumWalletManager + } } + } + + fun getWalletData(currencyName: CryptoCurrencyName?): WalletData? { + if (currencyName == null) return null + return wallets.find { it.currencyData.currencySymbol == currencyName } + } + + fun getSelectedWalletData(): WalletData? { + return wallets.find { it.currencyData.currencySymbol == selectedWallet } + } + + fun canBeRemoved(walletData: WalletData?): Boolean { + if (walletData == null) return false + + if (walletData.blockchain != store.state.walletState.primaryBlockchain + && (walletData.token == null || walletData.token != store.state.walletState.primaryToken)) { + val walletManager = getWalletManager(walletData.currencyData.currencySymbol) + ?: return true + val wallet = walletManager.wallet + if (walletData.blockchain != null) { + return wallet.recentTransactions.toPendingTransactions(wallet.address).isEmpty() && + wallet.amounts.toSendableAmounts().isEmpty() + } else if (walletData.token != null) ( + return wallet.recentTransactions.toPendingTransactionsForToken( + walletData.token, wallet.address).isEmpty() + && wallet.amounts[AmountType.Token(token = walletData.token)]?.isAboveZero() != true + ) + } + return false + } + + fun replaceWalletInWallets(walletData: WalletData?): List { + if (walletData == null) return wallets + return wallets.filter { it.currencyData.currency != walletData.currencyData.currency } + walletData + } + + fun replaceSomeWallets(newWallets: List): List { + val currencies = newWallets.map { it.currencyData.currency } + return wallets.filter { !currencies.contains(it.currencyData.currency) } + newWallets + } + } -sealed class WalletDialog { +sealed class WalletDialog: StateDialog { data class QrDialog( val qrCode: Bitmap?, val shareUrl: String?, val currencyName: CryptoCurrencyName? ) : WalletDialog() - data class CreatePayIdDialog(val creatingPayIdState: CreatingPayIdState?) : WalletDialog() data class SelectAmountToSendDialog(val amounts: List?) : WalletDialog() - data class WarningDialog(val type: WarningType) : WalletDialog() - data class TwinsOnboardingFragment(val secondCardId: String): WalletDialog() + object ScanFailsDialog: WalletDialog() } -enum class WarningType { CardSignedHashesBefore, DevCard } - - enum class ProgressState { Loading, Done, Error } enum class ErrorType { NoInternetConnection } -enum class PayIdState { Disabled, Loading, NotCreated, Created, ErrorLoading } - -data class PayIdData( - val payIdState: PayIdState = PayIdState.Loading, - val payId: String? = null -) - -enum class CreatingPayIdState { EnterPayId, Waiting } - sealed class WalletMainButton(enabled: Boolean) : Button(enabled) { class SendButton(enabled: Boolean) : WalletMainButton(enabled) class CreateWalletButton(enabled: Boolean) : WalletMainButton(enabled) @@ -83,7 +133,8 @@ data class AddressData( val address: String, val type: AddressType, val shareUrl: String, - val exploreUrl: String + val exploreUrl: String, + val qrCode: Bitmap? = null ) data class Artwork( @@ -112,4 +163,27 @@ data class TwinCardsState( val cardNumber: TwinCardNumber?, val showTwinOnboarding: Boolean, val isCreatingTwinCardsAllowed: Boolean -) \ No newline at end of file +) + +data class WalletData( + val pendingTransactions: List = emptyList(), + val hashesCountVerified: Boolean? = null, + val walletAddresses: WalletAddresses? = null, + val currencyData: BalanceWidgetData = BalanceWidgetData(), + val updatingWallet: Boolean = false, + val topUpState: TopUpState = TopUpState(), + val allowToSend: Boolean = true, + val fiatRateString: String? = null, + val fiatRate: BigDecimal? = null, + val mainButton: WalletMainButton = WalletMainButton.SendButton(false), + val token: Token? = null, + val blockchain: Blockchain? = null, +) { + fun shouldShowMultipleAddress(): Boolean { + val listOfAddresses = walletAddresses?.list ?: return false + return (currencyData.currencySymbol == Blockchain.Bitcoin.currency || + currencyData.currencySymbol == Blockchain.BitcoinTestnet.currency || + currencyData.currencySymbol == Blockchain.CardanoShelley.currency) && + listOfAddresses.size > 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt new file mode 100644 index 0000000000..6daa182e35 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -0,0 +1,134 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +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 +import com.tangem.tap.currenciesRepository +import com.tangem.tap.features.wallet.redux.WalletAction +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?, + ) { + 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)) + } + } + is WalletAction.MultiWallet.AddToken -> { + globalState?.scanNoteResponse?.card?.cardId?.let { + currenciesRepository.saveAddedToken(it, action.token) + } + addToken(action.token, walletState) + } + is WalletAction.MultiWallet.AddTokens -> { + action.tokens.map { addToken(it, walletState) } + } + is WalletAction.MultiWallet.AddBlockchain -> { + 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 + if (action.walletData.token != null) { + cardId?.let { currenciesRepository.removeToken(it, action.walletData.token) } + } else if (action.walletData.blockchain != null) { + 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 tokenFinder = walletManager as TokenFinder + scope.launch { + val result = tokenFinder.findTokens() + withContext(Dispatchers.Main) { + when (result) { + is Result.Success -> { + if (result.data.isNotEmpty()) { + store.dispatch(WalletAction.MultiWallet.AddTokens( + walletManager.presetTokens.toList() + )) + } + } + } + } + } + } + } + } + + private fun addToken(token: Token, walletState: WalletState?) { + val walletManager = walletState?.getWalletManager(token.symbol) + scope.launch { + val result = walletManager?.addToken(token) + withContext(Dispatchers.Main) { + when (result) { + is Result.Success -> { + store.dispatch(WalletAction.LoadFiatRate(currency = token.symbol)) + store.dispatch(WalletAction.MultiWallet.TokenLoaded(result.data)) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TopUpMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TopUpMiddleware.kt new file mode 100644 index 0000000000..e0706570f7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TopUpMiddleware.kt @@ -0,0 +1,32 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import android.net.Uri +import androidx.browser.customtabs.CustomTabsIntent +import com.tangem.tap.domain.TopUpHelper +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store + +class TopUpMiddleware { + fun handle(action: WalletAction.TopUpAction) { + when (action) { + is WalletAction.TopUpAction.TopUp -> { + val selectedWalletData = store.state.walletState.getSelectedWalletData() + val config = store.state.globalState.configManager?.config ?: return + val addresses = selectedWalletData?.walletAddresses ?: return + if (addresses.list.isEmpty()) return + + val defaultAddress = addresses.list[0].address + val url = TopUpHelper.getUrl( + selectedWalletData.currencyData.currencySymbol!!, + defaultAddress, + config.moonPayApiKey, + config.moonPayApiSecretKey + ) + val customTabsIntent = CustomTabsIntent.Builder() + .setToolbarColor(action.toolbarColor) + .build() + customTabsIntent.launchUrl(action.context, Uri.parse(url)); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TwinsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TwinsMiddleware.kt new file mode 100644 index 0000000000..88ba60d1d1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TwinsMiddleware.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.preferencesStorage +import com.tangem.tap.store + +class TwinsMiddleware { + fun handle(action: WalletAction.TwinsAction) { + when (action) { + is WalletAction.TwinsAction.SetTwinCard -> { + val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown() + if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding) + } + WalletAction.TwinsAction.SetOnboardingShown -> { + preferencesStorage.saveTwinsOnboardingShown() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt new file mode 100644 index 0000000000..05993a08ef --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -0,0 +1,239 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import android.content.Intent +import android.net.Uri +import androidx.core.content.ContextCompat +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 +import com.tangem.tap.domain.twins.TwinsHelper +import com.tangem.tap.features.details.redux.DetailsAction +import com.tangem.tap.features.details.redux.twins.CreateTwinWallet +import com.tangem.tap.features.send.redux.PrepareSendScreen +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.network.NetworkStateChanged +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.rekotlin.Action +import org.rekotlin.Middleware + +class WalletMiddleware { + private val topUpMiddleware = TopUpMiddleware() + private val twinsMiddleware = TwinsMiddleware() + private val warningsMiddleware = WarningsMiddleware() + private val multiWalletMiddleware = MultiWalletMiddleware() + + val walletMiddleware: Middleware = { dispatch, state -> + { next -> + { action -> + val globalState = state()?.globalState + val walletState = state()?.walletState + when (action) { + is WalletAction.TopUpAction -> topUpMiddleware.handle(action) + is WalletAction.TwinsAction -> twinsMiddleware.handle(action) + is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState) + is WalletAction.MultiWallet -> + multiWalletMiddleware.handle(action, walletState, globalState) + + is WalletAction.LoadWallet -> { + scope.launch { + if (action.currency == null) { + walletState?.walletManagers?.map { walletManager -> + globalState?.tapWalletManager?.loadWalletData(walletManager) + } + } else { + val walletManager = walletState?.getWalletManager(action.currency) + walletManager?.let { globalState?.tapWalletManager?.loadWalletData(it) } + } + } + } + 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) + } + is WalletAction.LoadFiatRate -> { + scope.launch { + if (action.wallet != null) { + globalState?.tapWalletManager?.loadFiatRate( + globalState.appCurrency, action.wallet + ) + } else if (action.currency != null) { + globalState?.tapWalletManager?.loadFiatRate( + globalState.appCurrency, action.currency + ) + } else { + walletState?.wallets?.filter { it.blockchain != null } + ?.map { walletState.getWalletManager(it.blockchain?.currency) } + ?.mapNotNull { walletManager -> + walletManager?.let { + globalState?.tapWalletManager?.loadFiatRate( + globalState.appCurrency, walletManager.wallet + ) + } + } + } + } + } + is WalletAction.CreateWallet -> { + if (walletState?.twinCardsState != null) { + store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning( + globalState?.scanNoteResponse?.card?.cardId?.let { + TwinsHelper.getTwinCardNumber(it) + }, + CreateTwinWallet.CreateWallet + )) + } else { + scope.launch { + val result = tangemSdkManager.createWallet( + globalState?.scanNoteResponse?.card?.cardId + ) + when (result) { + is CompletionResult.Success -> { + globalState?.tapWalletManager?.onCardScanned(result.data) + } + } + } + } + } + is WalletAction.UpdateWallet -> { + if (action.currency != null) { + scope.launch { + val walletManager = walletState?.getWalletManager(action.currency) + walletManager?.let { globalState?.tapWalletManager?.updateWallet(it) } + } + } else { + scope.launch { + if (walletState?.state == ProgressState.Done) { + walletState.walletManagers.map { walletManager -> + globalState?.tapWalletManager?.updateWallet(walletManager) + } + } + } + } + + } + is WalletAction.Scan -> { + scope.launch { + val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler) + 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) + } + } + } + } + } + is WalletAction.LoadData -> { + scope.launch { + val scanNoteResponse = globalState?.scanNoteResponse + ?: return@launch + if (!walletState?.wallets.isNullOrEmpty()) { + globalState.tapWalletManager.reloadData(scanNoteResponse) + } else { + globalState.tapWalletManager.loadData(scanNoteResponse) + } + } + } + is NetworkStateChanged -> { + globalState?.scanNoteResponse?.let { scanNoteResponse -> + store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline) + scope.launch { + globalState.tapWalletManager.loadData(scanNoteResponse) + } + } + } + is WalletAction.CopyAddress -> { + action.context.copyToClipboard(action.address) + store.dispatch(WalletAction.CopyAddress.Success) + } + is WalletAction.ShareAddress -> { + action.context.shareText(action.address) + } + is WalletAction.ExploreAddress -> { + val uri = Uri.parse(action.exploreUrl) + val intent = Intent(Intent.ACTION_VIEW, uri) + ContextCompat.startActivity(action.context, intent, null) + } + is WalletAction.Send -> { + val newAction = prepareSendAction(action.amount, state()?.walletState) + store.dispatch(newAction) + if (newAction is PrepareSendScreen) { + store.dispatch(NavigationAction.NavigateTo(AppScreen.Send)) + } + } + } + next(action) + } + } + } + + private fun prepareSendAction(amount: Amount?, state: WalletState?): Action { + val selectedWalletData = state?.getSelectedWalletData() + val currency = selectedWalletData?.currencyData?.currencySymbol + val walletManager = state?.getWalletManager(currency) + val wallet = walletManager?.wallet + + return if (amount != null) { + if (amount.type is AmountType.Token) { + prepareSendActionForToken(amount, state, selectedWalletData, wallet, walletManager) + } else { + PrepareSendScreen(amount, selectedWalletData?.fiatRate, walletManager) + } + } else { + val amounts = wallet?.amounts?.toSendableAmounts() + if (currency != null && state.isMultiwalletAllowed) { + val amountToSend = amounts?.find { it.currencySymbol == currency } + ?: return WalletAction.Send.ChooseCurrency(amounts) + if (amountToSend.type is AmountType.Token) { + prepareSendActionForToken(amountToSend, state, selectedWalletData, wallet, walletManager) + } else { + PrepareSendScreen(amountToSend, selectedWalletData.fiatRate, walletManager) + } + } else { + if (amounts?.size ?: 0 > 1) { + WalletAction.Send.ChooseCurrency(amounts) + } else { + val amountToSend = amounts?.first() + PrepareSendScreen(amountToSend, selectedWalletData?.fiatRate, walletManager) + } + } + } + } + + private fun prepareSendActionForToken( + 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) { + selectedWalletData?.fiatRate + } else { + selectedWalletData?.currencyData?.token?.fiatRate + } + return PrepareSendScreen( + wallet?.amounts?.get(AmountType.Coin), coinRate, walletManager, + amount, tokenRate) + } +} diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt new file mode 100644 index 0000000000..d158d3d323 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -0,0 +1,145 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.SignatureCountValidator +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 +import com.tangem.tap.common.extensions.isGreaterThan +import com.tangem.tap.common.redux.global.GlobalState +import com.tangem.tap.domain.configurable.warningMessage.WarningMessage +import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager +import com.tangem.tap.domain.isMultiwalletAllowed +import com.tangem.tap.domain.twins.isTwinCard +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.network.NetworkConnectivity +import com.tangem.tap.preferencesStorage +import com.tangem.tap.scope +import com.tangem.tap.store +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +class WarningsMiddleware { + fun handle(action: WalletAction.Warnings, globalState: GlobalState?) { + when (action) { + WalletAction.Warnings.Update -> setWarningMessages() + is WalletAction.Warnings.CheckIfNeeded -> { + showCardWarningsIfNeeded(globalState) + val readyToShow = preferencesStorage.appRatingLaunchObserver.isReadyToShow() + if (readyToShow) addWarningMessage(WarningMessagesManager.appRatingWarning(), true) + } + is WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline -> checkHashesCountOnline() + is WalletAction.Warnings.CheckHashesCount.SaveCardId -> { + val cardId = globalState?.scanNoteResponse?.card?.cardId + cardId?.let { preferencesStorage.saveScannedCardId(it) } + } + is WalletAction.Warnings.AppRating.RemindLater -> { + preferencesStorage.appRatingLaunchObserver.applyDelayedShowing() + } + is WalletAction.Warnings.AppRating.SetNeverToShow -> { + preferencesStorage.appRatingLaunchObserver.setNeverToShow() + } + } + } + + fun tryToShowAppRatingWarning(wallet: Wallet) { + val nonZeroWalletsCount = wallet.amounts.filter { + it.value.value?.isGreaterThan(BigDecimal.ZERO) ?: false + }.size + if (nonZeroWalletsCount > 0) { + preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds() + } + if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) { + FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_DISPLAYED) + addWarningMessage(WarningMessagesManager.appRatingWarning(), true) + } + } + + 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? { + if (card.isTwinCard() || card.isMultiwalletAllowed) return null + + return if (signatureCountValidator == null) { + if (card.walletSignedHashes ?: 0 > 0) { + WarningMessagesManager.alreadySignedHashesWarning() + } else { + store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) + null + } + } else { + store.dispatch(WalletAction.Warnings.CheckHashesCount.NeedToCheckHashesCountOnline) + null + } + } + + private fun checkHashesCountOnline() { + if (store.state.walletState.hashesCountVerified != false) return + if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return + + val card = store.state.globalState.scanNoteResponse?.card + if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return + + if (card.isTwinCard() || card.isMultiwalletAllowed) return + + val validator = store.state.globalState.scanNoteResponse?.walletManager + as? SignatureCountValidator + scope.launch { + val result = validator?.validateSignatureCount(card.walletSignedHashes ?: 0) + withContext(Dispatchers.Main) { + when (result) { + SimpleResult.Success -> { + store.dispatch(WalletAction.Warnings.CheckHashesCount.ConfirmHashesCount) + store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) + } + is SimpleResult.Failure -> + if (result.error is BlockchainSdkError.SignatureCountNotMatched) { + addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true) + } else if (card.walletSignedHashes ?: 0 > 0) { + addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true) + } + } + } + } + } + + private fun addWarningMessage(warning: WarningMessage, autoUpdate: Boolean = false) { + store.state.globalState.warningManager?.addWarning(warning) + if (autoUpdate) setWarningMessages() + } + + private fun setWarningMessages() { + store.dispatch(WalletAction.Warnings.Set(getWarnings())) + } + + private fun getWarnings(): List { + val warningManager = store.state.globalState.warningManager ?: return emptyList() + return warningManager.getWarnings(WarningMessage.Location.MainScreen, store.state.walletState.blockchains) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt new file mode 100644 index 0000000000..158d7e61ff --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt @@ -0,0 +1,169 @@ +package com.tangem.tap.features.wallet.redux.reducers + +import com.tangem.blockchain.common.AmountType +import com.tangem.common.extensions.isZero +import com.tangem.tap.common.extensions.toFiatString +import com.tangem.tap.common.extensions.toFormattedCurrencyString +import com.tangem.tap.domain.getFirstToken +import com.tangem.tap.features.wallet.models.removeUnknownTransactions +import com.tangem.tap.features.wallet.models.toPendingTransactions +import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.features.wallet.ui.BalanceWidgetData +import com.tangem.tap.features.wallet.ui.TokenData +import com.tangem.tap.store + +class MultiWalletReducer { + fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState { + return when (action) { + is WalletAction.MultiWallet.AddWalletManagers -> { + state.copy( + walletManagers = state.walletManagers + action.walletManagers + ) + } + is WalletAction.MultiWallet.AddBlockchains -> { + val wallets = action.blockchains.map { blockchain -> + val wallet = state.getWalletManager(blockchain.currency)?.wallet + val cardToken = if (!state.isMultiwalletAllowed) { + wallet?.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) } + } else { + null + } + WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + blockchain.fullName, + currencySymbol = blockchain.currency, + token = cardToken + ), + walletAddresses = createAddressList(wallet), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + blockchain = blockchain + ) + } + + val selectedWallet = if (!state.isMultiwalletAllowed) { + wallets[0].currencyData.currencySymbol + } else { + state.selectedWallet + } + state.copy( + wallets = wallets, + selectedWallet = selectedWallet + ) + } + is WalletAction.MultiWallet.AddBlockchain -> { + val wallet = state.getWalletManager(action.blockchain.currency)?.wallet + val walletData = WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + action.blockchain.fullName, + currencySymbol = action.blockchain.currency, + ), + walletAddresses = createAddressList(wallet), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + blockchain = action.blockchain + ) + val newState = state.copy(wallets = state.replaceWalletInWallets(walletData)) + if (wallet != null && wallet.amounts[AmountType.Coin]?.value != null) { + OnWalletLoadedReducer().reduce(wallet, newState) + } else { + newState + } + } + is WalletAction.MultiWallet.AddTokens -> { + if (!state.isMultiwalletAllowed) return state + val wallets = action.tokens.map { token -> + WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + currency = token.name, + currencySymbol = token.symbol + ), + walletAddresses = createAddressList( + state.getWalletManagerForToken(token.symbol)?.wallet + ), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + token = token + ) + } + state.copy(wallets = state.replaceSomeWallets(wallets)) + } + is WalletAction.MultiWallet.AddToken -> { + if (!state.isMultiwalletAllowed) return state + + val walletAddresses = createAddressList( + state.getWalletManagerForToken(action.token.symbol)?.wallet + + ) + val wallet = WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + currency = action.token.name, + currencySymbol = action.token.symbol + ), + walletAddresses = walletAddresses, + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + token = action.token + ) + val wallets = state.replaceWalletInWallets(wallet) + state.copy(wallets = wallets) + } + is WalletAction.MultiWallet.TokenLoaded -> { + 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 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 = tokenBalanceStatus, + amount = action.amount.value?.toFormattedCurrencyString( + action.amount.decimals, action.amount.currencySymbol + ), + fiatAmount = tokenWalletData.fiatRate?.let { + action.amount.value + ?.toFiatString(it, store.state.globalState.appCurrency) + } + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled) + ) + val wallets = state.replaceWalletInWallets(newTokenWalletData) + state.copy(wallets = wallets) + } + is WalletAction.MultiWallet.SetIsMultiwalletAllowed -> + state.copy(isMultiwalletAllowed = action.isMultiwalletAllowed) + + is WalletAction.MultiWallet.SelectWallet -> + state.copy(selectedWallet = action.walletData?.currencyData?.currencySymbol) + + is WalletAction.MultiWallet.RemoveWallet -> { + state.copy(wallets = state.wallets.filterNot { + it.currencyData.currencySymbol == action.walletData.currencyData.currencySymbol + }) + } + is WalletAction.MultiWallet.SetPrimaryBlockchain -> + state.copy(primaryBlockchain = action.blockchain) + + is WalletAction.MultiWallet.SetPrimaryToken -> + state.copy(primaryToken = action.token) + is WalletAction.MultiWallet.FindTokensInUse -> state + is WalletAction.MultiWallet.FindBlockchainsInUse -> state + is WalletAction.MultiWallet.SaveCurrencies -> state + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt new file mode 100644 index 0000000000..2b3ad57454 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt @@ -0,0 +1,155 @@ +package com.tangem.tap.features.wallet.redux.reducers + +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Wallet +import com.tangem.common.extensions.isZero +import com.tangem.tap.common.extensions.toFiatString +import com.tangem.tap.common.extensions.toFormattedCurrencyString +import com.tangem.tap.common.extensions.toFormattedString +import com.tangem.tap.domain.getFirstToken +import com.tangem.tap.features.wallet.models.removeUnknownTransactions +import com.tangem.tap.features.wallet.models.toPendingTransactions +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletMainButton +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.features.wallet.ui.BalanceWidgetData +import com.tangem.tap.features.wallet.ui.TokenData +import com.tangem.tap.store +import java.math.RoundingMode + +class OnWalletLoadedReducer { + + fun reduce(wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null): WalletState { + return if (!walletState.isMultiwalletAllowed) { + onSingleWalletLoaded(wallet, walletState, topUpAllowed) + } else { + onMultiWalletLoaded(wallet, walletState, topUpAllowed) + } + } + + 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 + + val fiatCurrencySymbol = store.state.globalState.appCurrency + val token = wallet.getFirstToken() + val tokenData = if (token != null) { + val tokenAmount = wallet.getTokenAmount(token) + if (tokenAmount != null) { + val tokenFiatRate = walletState.primaryWallet?.currencyData?.token?.fiatRate + val tokenFiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencySymbol) } + TokenData(tokenAmount.value?.toFormattedString(tokenAmount.decimals) ?: "", + tokenAmount.currencySymbol, tokenFiatAmount) + } else { + null + } + } else { + null + } + val amount = wallet.amounts[AmountType.Coin]?.value + val formattedAmount = amount?.toFormattedCurrencyString( + wallet.blockchain.decimals(), + wallet.blockchain.currency) + val fiatRate = walletState.primaryWallet?.fiatRate + val fiatAmountRaw = fiatRate?.multiply(amount)?.setScale(2, RoundingMode.DOWN) + val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) } + + 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.primaryWallet?.copy( + currencyData = BalanceWidgetData( + balanceStatus, wallet.blockchain.fullName, + currencySymbol = wallet.blockchain.currency, + formattedAmount, + token = tokenData, + fiatAmount = fiatAmount, + fiatAmountRaw = fiatAmountRaw + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled) + ) + val wallets = walletData?.let { listOf(walletData) } ?: emptyList() + return walletState.copy( + state = ProgressState.Done, wallets = wallets, error = null + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TwinsReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TwinsReducer.kt new file mode 100644 index 0000000000..79025ca6cc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TwinsReducer.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.features.wallet.redux.reducers + +import com.tangem.tap.features.wallet.redux.TwinCardsState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletState + +class TwinsReducer { + fun reduce(action: WalletAction.TwinsAction, state: WalletState): WalletState { + return when (action) { + is WalletAction.TwinsAction.SetTwinCard -> { + state.copy( + twinCardsState = TwinCardsState( + secondCardId = action.secondCardId, + cardNumber = action.number, + showTwinOnboarding = state.twinCardsState?.showTwinOnboarding + ?: false, + isCreatingTwinCardsAllowed = action.isCreatingTwinCardsAllowed + ) + ) + } + is WalletAction.TwinsAction.ShowOnboarding -> { + state.copy( + twinCardsState = state.twinCardsState?.copy(showTwinOnboarding = true) + ?: TwinCardsState(null, null, + showTwinOnboarding = true, + isCreatingTwinCardsAllowed = false) + ) + } + is WalletAction.TwinsAction.SetOnboardingShown -> { + state.copy( + twinCardsState = state.twinCardsState?.copy(showTwinOnboarding = false) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt new file mode 100644 index 0000000000..957e9a32d3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -0,0 +1,361 @@ +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.commands.common.network.TangemService +import com.tangem.common.extensions.toHexString +import com.tangem.tap.common.extensions.toFiatString +import com.tangem.tap.common.extensions.toFormattedCurrencyString +import com.tangem.tap.common.extensions.toQrCode +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.common.redux.global.FiatCurrencyName +import com.tangem.tap.domain.TapError +import com.tangem.tap.domain.getFirstToken +import com.tangem.tap.domain.twins.TwinCardNumber +import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.features.wallet.ui.BalanceWidgetData +import org.rekotlin.Action +import java.math.BigDecimal + +class WalletReducer { + companion object { + fun reduce(action: Action, state: AppState): WalletState = internalReduce(action, state) + } +} + +private fun internalReduce(action: Action, state: AppState): WalletState { + + val multiWalletReducer = MultiWalletReducer() + val twinsReducer = TwinsReducer() + val onWalletLoadedReducer = OnWalletLoadedReducer() + + if (action !is WalletAction) return state.walletState + + var newState = state.walletState + + when (action) { + is WalletAction.Warnings -> newState = handleCheckSignedHashesActions(action, newState) + is WalletAction.TwinsAction -> newState = twinsReducer.reduce(action, newState) + is WalletAction.MultiWallet -> newState = multiWalletReducer.reduce(action, newState) + + is WalletAction.ResetState -> newState = WalletState() + is WalletAction.EmptyWallet -> { + val creatingWalletAllowed = !(newState.twinCardsState != null && + newState.twinCardsState?.isCreatingTwinCardsAllowed != true) + + newState = newState.copy( + state = ProgressState.Done, + wallets = listOf(WalletData( + currencyData = BalanceWidgetData(BalanceStatus.EmptyCard), + mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed), + topUpState = TopUpState(false) + ))) + } + 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 -> { + newState = newState.copy( + state = ProgressState.Done, + wallets = listOf(WalletData( + currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain), + topUpState = TopUpState(false) + )) + ) + } + } + } + + is WalletAction.LoadData -> { + newState = newState.copy( + state = ProgressState.Loading, + error = null, + ) + } + is WalletAction.LoadWallet -> { + if (action.currency == null) { + val wallets = newState.wallets.map { wallet -> + wallet.copy( + currencyData = wallet.currencyData.copy( + status = BalanceStatus.Loading, + currency = wallet.currencyData.currency, + currencySymbol = wallet.currencyData.currencySymbol, + ), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = action.allowTopUp + ?: wallet.topUpState.allowed) + ) + } + newState = newState.copy( + state = ProgressState.Loading, + wallets = wallets + ) + } else { + val walletManager = newState.getWalletManager(action.currency) ?: return newState + 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( + currencyData = wallet.currencyData.copy( + status = BalanceStatus.Loading, + currency = wallet.currencyData.currency, + currencySymbol = wallet.currencyData.currencySymbol, + ), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState( + allowed = action.allowTopUp ?: wallet.topUpState.allowed + ) + ) + } + 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) + } + is WalletAction.LoadWallet.NoAccount -> { + val walletData = newState.getWalletData(action.wallet.blockchain.currency)?.copy( + currencyData = BalanceWidgetData( + BalanceStatus.NoAccount, action.wallet.blockchain.fullName, + currencySymbol = action.wallet.blockchain.currency, + amountToCreateAccount = action.amountToCreateAccount + ) + ) + val wallets = newState.replaceWalletInWallets(walletData) + val progressState = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) { + ProgressState.Loading + } else { + ProgressState.Done + } + newState = newState.copy( + 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 = message + ), + topUpState = TopUpState(false) + ) + 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 = progressState, wallets = wallets + ) + } + is WalletAction.SetArtworkId -> { + val cardImage = if (newState.cardImage?.artworkId == action.artworkId) { + newState.cardImage + } else { + null + } + newState = newState.copy(cardImage = cardImage) + } + + is WalletAction.LoadFiatRate.Success -> + newState = setNewFiatRate(action.fiatRate, state.globalState.appCurrency, newState) + is WalletAction.LoadArtwork -> { + val cardId = action.card.cardId + val cardPublicKey = action.card.cardPublicKey?.toHexString() + val artworkUrl = if (cardPublicKey != null && action.artworkId != null) { + TangemService.getUrlForArtwork(cardId, cardPublicKey, action.artworkId) + } else if (action.card.cardId.startsWith(Artwork.SERGIO_CARD_ID)) { + Artwork.SERGIO_CARD_URL + } else if (action.card.cardId.startsWith(Artwork.MARTA_CARD_ID)) { + Artwork.MARTA_CARD_URL + } else if (newState.twinCardsState?.cardNumber != null) { + when (newState.twinCardsState?.cardNumber) { + TwinCardNumber.First -> Artwork.TWIN_CARD_1 + TwinCardNumber.Second -> Artwork.TWIN_CARD_2 + null -> Artwork.DEFAULT_IMG_URL + } + } else { + Artwork.DEFAULT_IMG_URL + } + newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl)) + } + is WalletAction.ShowDialog.QrCode -> { + val selectedWalletData = newState.getWalletData(newState.selectedWallet) + newState = newState.copy( + walletDialog = WalletDialog.QrDialog( + selectedWalletData?.walletAddresses?.selectedAddress?.shareUrl?.toQrCode(), + selectedWalletData?.walletAddresses?.selectedAddress?.shareUrl, + selectedWalletData?.currencyData?.currency + ) + ) + } + is WalletAction.ShowDialog.ScanFails -> { + newState = newState.copy(walletDialog = WalletDialog.ScanFailsDialog) + } + is WalletAction.HideDialog -> { + newState = newState.copy(walletDialog = null) + } + is WalletAction.Send.ChooseCurrency -> { + newState = newState.copy( + walletDialog = WalletDialog.SelectAmountToSendDialog(action.amounts) + ) + } + is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null) + is WalletAction.TopUpAction -> return newState + is WalletAction.ChangeSelectedAddress -> { + val selectedWalletData = newState.getWalletData(newState.selectedWallet) + + val walletAddresses = newState.getWalletData(selectedWalletData?.currencyData?.currencySymbol)?.walletAddresses + ?: return newState + val address = walletAddresses.list.firstOrNull { it.type == action.type } + ?: return newState + val wallets = newState.replaceWalletInWallets( + selectedWalletData?.copy(walletAddresses = WalletAddresses(address, walletAddresses.list)) + ) + newState = newState.copy(wallets = wallets) + } + } + return newState +} + +fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null): WalletAddresses? { + if (wallet == null) return null + + val listOfAddressData = mutableListOf() + // put a defaultAddress at the first place + wallet.addresses.forEach { + val addressData = AddressData(it.value, it.type, wallet.getShareUri(it.value), wallet.getExploreUrl(it.value)) + if (it.type == wallet.blockchain.defaultAddressType()) { + listOfAddressData.add(0, addressData) + } else { + listOfAddressData.add(addressData) + } + } + + // restore a selected wallet address + var indexOfSelectedWallet = 0 + walletAddresses?.let { + val index = listOfAddressData.indexOfFirst { it.address == walletAddresses.selectedAddress.address } + if (index != -1) indexOfSelectedWallet = index + } + return WalletAddresses(listOfAddressData[indexOfSelectedWallet], listOfAddressData) +} + +private fun handleCheckSignedHashesActions(action: WalletAction.Warnings, state: WalletState): WalletState { + return when (action) { + WalletAction.Warnings.CheckHashesCount.ConfirmHashesCount -> state.copy(hashesCountVerified = true) + WalletAction.Warnings.CheckHashesCount.NeedToCheckHashesCountOnline -> state.copy(hashesCountVerified = false) + is WalletAction.Warnings.Set -> state.copy(mainWarningsList = action.warningList) + else -> state + } +} + + +private fun setNewFiatRate( + fiatRate: Pair, + appCurrency: FiatCurrencyName, state: WalletState +): WalletState { + val rate = fiatRate.second ?: return state + val rateFormatted = rate.toFormattedCurrencyString(2, appCurrency) + val currency = fiatRate.first + + if (!state.isMultiwalletAllowed) { + return setSingeWalletFiatRate(rate, rateFormatted, currency, appCurrency, state) + } else { + return setMultiWalletFiatRate(rate, rateFormatted, currency, appCurrency, state) + } +} + +private fun setMultiWalletFiatRate( + rate: BigDecimal, rateFormatted: String, currency: CryptoCurrencyName, + appCurrency: FiatCurrencyName, state: WalletState +): WalletState { + val walletData = state.getWalletData(currency) ?: return state + val wallet = state.walletManagers.find { it.wallet.blockchain.currency == currency }?.wallet + if (wallet != null) { + val newWalletData = state.getWalletData(currency)?.copy( + currencyData = walletData.currencyData.copy( + fiatAmount = wallet.amounts[AmountType.Coin]?.value + ?.toFiatString(rate, appCurrency)), + fiatRate = rate, fiatRateString = rateFormatted + ) + return state.copy(wallets = state.replaceWalletInWallets(newWalletData)) + } else { + val ethereumWallet = state.walletManagers.find { it.wallet.blockchain == Blockchain.Ethereum }?.wallet + val token = ethereumWallet?.getTokens()?.find { it.symbol == currency } ?: return state + val tokenFiatAmount = ethereumWallet.getTokenAmount(token)?.value?.toFiatString(rate, appCurrency) + val newWalletData = state.getWalletData(currency)?.copy( + currencyData = walletData.currencyData.copy( + fiatAmount = tokenFiatAmount), + fiatRate = rate, fiatRateString = rateFormatted + ) + return state.copy(wallets = state.replaceWalletInWallets(newWalletData)) + } +} + +private fun setSingeWalletFiatRate( + rate: BigDecimal, rateFormatted: String, currency: CryptoCurrencyName, + appCurrency: FiatCurrencyName, state: WalletState +): WalletState { + val wallet = state.walletManagers[0].wallet + val token = wallet.getFirstToken() + + if (currency == state.primaryWallet?.currencyData?.currencySymbol) { + val fiatAmount = wallet.amounts[AmountType.Coin]?.value + ?.toFiatString(rate, appCurrency) + val walletData = state.primaryWallet.copy( + currencyData = state.primaryWallet.currencyData.copy(fiatAmount = fiatAmount), + fiatRate = rate, + fiatRateString = rateFormatted + ) + return state.copy(wallets = listOf(walletData)) + } else if (currency == token?.symbol) { + val tokenFiatAmount = wallet.getTokenAmount(token)?.value?.toFiatString(rate, appCurrency) + val tokenData = state.primaryWallet?.currencyData?.token?.copy( + fiatAmount = tokenFiatAmount, + fiatRate = rate, + fiatRateString = rateFormatted + ) + + val walletData = state.primaryWallet?.copy( + currencyData = state.primaryWallet.currencyData.copy( + token = tokenData + )) + val wallets = walletData?.let { listOf(walletData) } ?: emptyList() + return state.copy(wallets = wallets) + } + return state +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index ce7fec6959..508a62b08c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -8,10 +8,12 @@ import com.tangem.wallet.R import kotlinx.android.synthetic.main.card_balance.* import kotlinx.android.synthetic.main.layout_balance.* import kotlinx.android.synthetic.main.layout_balance_error.* +import java.math.BigDecimal enum class BalanceStatus { VerifiedOnline, TransactionInProgress, + SameCurrencyTransactionInProgress, Unreachable, Loading, NoAccount, @@ -25,6 +27,7 @@ data class BalanceWidgetData( val currencySymbol: String? = null, val amount: String? = null, val fiatAmount: String? = null, + val fiatAmountRaw: BigDecimal? = null, val token: TokenData? = null, val amountToCreateAccount: String? = null, val errorMessage: String? = null @@ -33,14 +36,16 @@ data class BalanceWidgetData( data class TokenData( val amount: String, val tokenSymbol: String, - val fiatAmount: String? = null + val fiatAmount: String? = null, + val fiatRateString: String? = null, + val fiatRate: BigDecimal? = null, ) class BalanceWidget( val fragment: Fragment, val data: BalanceWidgetData, - val isTwinCard: Boolean, + private val isTwinCard: Boolean, ) { fun setup() { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt new file mode 100644 index 0000000000..66425c74a7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt @@ -0,0 +1,42 @@ +package com.tangem.tap.features.wallet.ui + +import android.view.View +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.wallet.R + +class MultipleAddressUiHelper { + companion object { + fun typeToId(type: AddressType): Int { + return when (type) { + is BitcoinAddressType.Legacy -> R.id.chip_legacy + is BitcoinAddressType.Segwit -> R.id.chip_default + is CardanoAddressType.Byron -> R.id.chip_legacy + is CardanoAddressType.Shelley -> R.id.chip_default + else -> View.NO_ID + } + } + + fun idToType(id: Int, blockchain: Blockchain?): AddressType? { + return when (id) { + R.id.chip_default -> { + when (blockchain) { + Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> BitcoinAddressType.Segwit + Blockchain.CardanoShelley -> CardanoAddressType.Shelley + else -> null + } + } + R.id.chip_legacy -> { + when (blockchain) { + Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> BitcoinAddressType.Legacy + Blockchain.CardanoShelley -> CardanoAddressType.Byron + else -> null + } + } + else -> null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt new file mode 100644 index 0000000000..5b59b03015 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -0,0 +1,303 @@ +package com.tangem.tap.features.wallet.ui + +import android.app.Dialog +import android.os.Bundle +import android.view.* +import androidx.activity.OnBackPressedCallback +import androidx.annotation.IdRes +import androidx.appcompat.app.AppCompatActivity +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.* +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_details.* +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.* +import kotlinx.android.synthetic.main.layout_balance_wallet_details.* +import kotlinx.android.synthetic.main.layout_wallet_details.* +import org.rekotlin.StoreSubscriber + +class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreSubscriber { + + private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter + private var dialog: Dialog? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + store.dispatch(WalletAction.MultiWallet.SelectWallet(null)) + store.dispatch(NavigationAction.PopBackTo()) + } + }) + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(R.transition.slide_right) + exitTransition = inflater.inflateTransition(R.transition.fade) + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.walletState == newState.walletState + }.select { it.walletState } + } + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + toolbar.setNavigationOnClickListener { activity?.onBackPressed() } + + + setupTransactionsRecyclerView() + setupButtons() + } + + + private fun setupTransactionsRecyclerView() { + pendingTransactionAdapter = PendingTransactionsAdapter() + rv_pending_transaction.layoutManager = LinearLayoutManager(requireContext()) + rv_pending_transaction.adapter = pendingTransactionAdapter + } + + private fun setupButtons() { + btn_confirm.text = getString(R.string.wallet_button_send) + btn_confirm.setOnClickListener { store.dispatch(WalletAction.Send()) } + + btn_share.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) } + + btn_top_up.setOnClickListener { + store.dispatch( + WalletAction.TopUpAction.TopUp(requireContext(), R.color.backgroundLightGray) + ) + } + } + + override fun newState(state: WalletState) { + if (activity == null) return + if (state.selectedWallet == null) return + val selectedWallet = state.getSelectedWalletData() ?: return + + tv_currency_title.text = selectedWallet.currencyData.currency + + showPendingTransactionsIfPresent(selectedWallet.pendingTransactions) + setupAddressCard(selectedWallet) + setupNoInternetHandling(state) + setupBalanceData(selectedWallet.currencyData) + + btn_confirm.isEnabled = selectedWallet.mainButton.enabled + btn_copy.setOnClickListener { + selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> + store.dispatch(WalletAction.CopyAddress(addressString, requireContext())) + } + } + btn_share.setOnClickListener { + selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> + store.dispatch(WalletAction.ShareAddress(addressString, requireContext())) + } + } + + handleDialogs(state.walletDialog) + handleCurrencyIcon(selectedWallet) + + srl_wallet_details.setOnRefreshListener { + if (selectedWallet.currencyData.status != BalanceStatus.Loading) { + store.dispatch(WalletAction.LoadWallet( + selectedWallet.topUpState.allowed, + selectedWallet.currencyData.currencySymbol + )) + } + } + + if (selectedWallet.currencyData.status != BalanceStatus.Loading) { + srl_wallet_details.isRefreshing = false + } + } + + private fun handleCurrencyIcon(wallet: WalletData) { + val blockchain = wallet.currencyData.currencySymbol?.let { Blockchain.fromCurrency(it) } + if (blockchain != null && blockchain != Blockchain.Unknown) { + tv_token_letter.text = null + iv_currency.colorFilter = null + iv_currency.setImageResource(blockchain.getIconRes()) + } else { + tv_token_letter.text = wallet.currencyData.currencySymbol?.take(1) + wallet.token?.getColor()?.let { iv_currency.setColorFilter(it) } + iv_currency.setImageResource(R.drawable.shape_circle) + } + } + + + private fun showPendingTransactionsIfPresent(pendingTransactions: List) { + pendingTransactionAdapter.submitList(pendingTransactions) + rv_pending_transaction?.show(pendingTransactions.isNotEmpty()) + } + + private fun setupAddressCard(state: WalletData) { + if (state.walletAddresses != null) { + if (state.shouldShowMultipleAddress() && state.blockchain != null) { + (card_balance as? ViewGroup)?.beginDelayedTransition() + chip_group_address_type.show() + chip_group_address_type.fitChipsByGroupWidth() + + val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) + if (checkedId != View.NO_ID) chip_group_address_type.check(checkedId) + + chip_group_address_type.setOnCheckedChangeListener { group, checkedId -> + if (checkedId == -1) return@setOnCheckedChangeListener + val type = MultipleAddressUiHelper.idToType(checkedId, state.blockchain) + type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) } + } + } else { + chip_group_address_type.hide() + } + tv_address.text = state.walletAddresses.selectedAddress.address + tv_explore?.setOnClickListener { + store.dispatch(WalletAction.ExploreAddress( + state.walletAddresses.selectedAddress.exploreUrl, + requireContext())) + } + 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() + } + } + + private fun setupBalanceData(data: BalanceWidgetData) { + when (data.status) { + BalanceStatus.Loading -> { + l_balance.show() + l_balance_error.hide() + tv_fiat_amount.hide() + tv_amount.text = "" + + showStatus(R.id.tv_status_loading) + showBalanceWithoutToken(data, false) + } + BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress, + BalanceStatus.TransactionInProgress -> { + l_balance.show() + l_balance_error.hide() + val statusView = if (data.status == BalanceStatus.VerifiedOnline || + data.status == BalanceStatus.SameCurrencyTransactionInProgress) { + R.id.tv_status_verified + } else { + tv_status_error.text = + getText(R.string.wallet_balance_tx_in_progress) + R.id.group_error + } + showStatus(statusView) + tv_status_error_message.hide() + showBalanceWithoutToken(data, true) + } + BalanceStatus.Unreachable -> { + l_balance.show() + l_balance_error.hide() + tv_fiat_amount.hide() + + tv_amount.text = "" + + tv_status_error_message.text = data.errorMessage + tv_status_error.text = + getString(R.string.wallet_balance_blockchain_unreachable) + + showStatus(R.id.group_error) + tv_status_error_message.show(!data.errorMessage.isNullOrBlank()) + } + + BalanceStatus.NoAccount -> { + l_balance.hide() + l_balance_error.show() + tv_error_title.text = getText(R.string.wallet_error_no_account) + tv_error_descriptions.text = + getString( + R.string.wallet_error_no_account_subtitle_format, + data.amountToCreateAccount, data.currencySymbol + ) + } + } + card_pending_transaction_warning.show(data.status == BalanceStatus.SameCurrencyTransactionInProgress) + } + + private fun showStatus(@IdRes viewRes: Int) { + group_error.show(viewRes == R.id.group_error) + tv_status_loading.show(viewRes == R.id.tv_status_loading) + tv_status_verified.show(viewRes == R.id.tv_status_verified) + } + + private fun showBalanceWithoutToken(data: BalanceWidgetData, showAmount: Boolean) { + tv_amount.text = if (showAmount) data.amount else "" + if (showAmount) { + tv_fiat_amount.show() + tv_fiat_amount.text = data.fiatAmount + } + } + + private fun handleDialogs(walletDialog: StateDialog?) { + when (walletDialog) { + is WalletDialog.SelectAmountToSendDialog -> { + if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply { + this.show(walletDialog.amounts) + } + } + null -> { + dialog?.dismiss() + dialog = null + } + } + } + + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.meny_remove -> { + store.state.walletState.getSelectedWalletData()?.let { walletData -> + store.dispatch(WalletAction.MultiWallet.RemoveWallet(walletData)) + store.dispatch(WalletAction.MultiWallet.SelectWallet(null)) + store.dispatch(NavigationAction.PopBackTo()) + true + } + false + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + if (store.state.walletState.canBeRemoved(store.state.walletState.getSelectedWalletData())) { + inflater.inflate(R.menu.wallet_details, menu) + } + } + +} diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index eedcf10ca5..580bb34141 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -1,48 +1,45 @@ package com.tangem.tap.features.wallet.ui -import android.app.Dialog import android.os.Bundle -import android.view.* +import android.view.Menu import android.view.Menu.NONE +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity 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.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.extensions.* +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 +import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.termsOfUse.CardTou -import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.wallet.redux.* -import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog -import com.tangem.tap.features.wallet.ui.dialogs.PayIdDialog -import com.tangem.tap.features.wallet.ui.dialogs.QrDialog -import com.tangem.tap.features.wallet.ui.dialogs.WarningDialog +import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration +import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter +import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView +import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView +import com.tangem.tap.features.wallet.ui.wallet.WalletView import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.card_balance.* import kotlinx.android.synthetic.main.fragment_wallet.* -import kotlinx.android.synthetic.main.layout_address.* -import kotlinx.android.synthetic.main.layout_send_fee.* -import kotlinx.android.synthetic.main.layout_wallet_long_buttons.* -import kotlinx.android.synthetic.main.layout_wallet_short_buttons.* +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 { - private var dialog: Dialog? = null - private var snackbar: Snackbar? = null + private lateinit var warningsAdapter: WarningMessagesAdapter - private lateinit var viewAdapter: PendingTransactionsAdapter + private var walletView: WalletView = SingleWalletView() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -64,12 +61,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber - tv_twin_card_number.show() - iv_twin_card.show() - val number = when (cardNumber) { - TwinCardNumber.First -> "1" - TwinCardNumber.Second -> "2" - } - tv_twin_card_number.text = getString(R.string.wallet_twins_chip_format, number) - } - if (state.twinCardsState?.cardNumber == null) { - tv_twin_card_number.hide() - iv_twin_card.hide() + if (state.isMultiwalletAllowed && walletView is SingleWalletView) { + walletView = MultiWalletView() + walletView.changeWalletView(this) + } else if (!state.isMultiwalletAllowed && walletView is MultiWalletView) { + walletView = SingleWalletView() + walletView.changeWalletView(this) } + walletView.onNewState(state) - if (!state.showDetails) { + if (!state.shouldShowDetails) { toolbar.menu.removeItem(R.id.details_menu) } else if (toolbar.menu.findItem(R.id.details_menu) == null) { toolbar.menu.add(R.menu.wallet, R.id.details_menu, NONE, R.string.details_title) } - srl_wallet.setOnRefreshListener { - store.dispatch(WalletAction.LoadData) - } - setupNoInternetHandling(state) - - setupButtons(state) - setupAddressCard(state) setupCardImage(state.cardImage) - viewAdapter.submitList(state.pendingTransactions) - if (state.pendingTransactions.isEmpty()) { - rv_pending_transaction.hide() - } else { - rv_pending_transaction.show() + showWarningsIfPresent(state.mainWarningsList) + + srl_wallet.setOnRefreshListener { + if (state.state != ProgressState.Loading) { + store.dispatch(WalletAction.LoadData) + } } - handleDialogs(state.walletDialog) - - l_balance.show() - BalanceWidget(this, state.currencyData, state.twinCardsState != null).setup() - - if (state.state == ProgressState.Done) { + if (state.state != ProgressState.Loading) { srl_wallet.isRefreshing = false } + } - when (state.payIdData.payIdState) { - PayIdState.Disabled, PayIdState.Loading -> group_payid.hide() - PayIdState.NotCreated -> { - group_payid.show() - tv_create_payid.show() - tv_create_payid.setOnClickListener { store.dispatch(WalletAction.CreatePayId) } - tv_payid_address.hide() - } - PayIdState.Created -> { - group_payid.show() - tv_create_payid.hide() - tv_payid_address.show() - tv_payid_address.text = state.payIdData.payId - - } - PayIdState.ErrorLoading -> { - } - } + private fun showWarningsIfPresent(warnings: List) { + warningsAdapter.submitList(warnings) + rv_warning_messages.show(warnings.isNotEmpty()) } 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) } - .also { it.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() - } - } - - private fun setupButtons(state: WalletState) { - if (state.topUpState.allowed) { - l_buttons_long.hide() - l_buttons_short.show() - } else { - l_buttons_long.show() - l_buttons_short.hide() - } - - val btnConfirm = if (state.topUpState.allowed) { - btn_confirm_short - } else { - btn_confirm_long - } - val btnScan = if (state.topUpState.allowed) { - btn_scan_short - } else { - btn_scan_long - } - - btnScan.setOnClickListener { - store.dispatch(WalletAction.Scan) - } - - btn_copy.setOnClickListener { store.dispatch(WalletAction.CopyAddress(requireContext())) } - btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) } - - val buttonTitle = when (state.mainButton) { - is WalletMainButton.SendButton -> R.string.wallet_button_send - is WalletMainButton.CreateWalletButton -> { - if (state.twinCardsState == null) { - R.string.wallet_button_create_wallet - } else { - R.string.wallet_button_create_twin_wallet - } - } - } - btnConfirm.text = getString(buttonTitle) - btnConfirm.isEnabled = state.mainButton.enabled - - btnConfirm.setOnClickListener { - when (state.mainButton) { - is WalletMainButton.SendButton -> store.dispatch(WalletAction.Send()) - is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet) - } - } - btn_top_up.setOnClickListener { - store.dispatch( - WalletAction.TopUpAction.TopUp(requireContext(), R.color.backgroundLightGray) - ) - } - } - - private fun setupAddressCard(state: WalletState) { - if (state.walletAddresses != null) { - l_address?.show() - if (state.showMultipleAddress) { - (l_address as? ViewGroup)?.beginDelayedTransition() - chip_group_address_type.show() - chip_group_address_type.fitChipsByGroupWidth() - - val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) - if (checkedId != View.NO_ID) chip_group_address_type.check(checkedId) - - chip_group_address_type.setOnCheckedChangeListener { group, checkedId -> - if (checkedId == -1) return@setOnCheckedChangeListener - - MultipleAddressUiHelper.idToType(checkedId, state.wallet?.blockchain)?.let { - store.dispatch(WalletAction.ChangeSelectedAddress(it)) - } - } - } else { - chip_group_address_type.hide() - } - tv_address.text = state.walletAddresses.selectedAddress.address - tv_explore?.setOnClickListener { - store.dispatch(WalletAction.ExploreAddress(requireContext())) - } - } else { - l_address?.hide() + (activity as? MainActivity)?.dismissSnackbar() } } @@ -266,52 +152,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { - if (walletDialog.qrCode != null && walletDialog.shareUrl != null) { - if (dialog == null) dialog = QrDialog(requireContext()).apply { - this.showQr( - walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName - ) - } - } - } - is WalletDialog.CreatePayIdDialog -> { - when (walletDialog.creatingPayIdState) { - CreatingPayIdState.EnterPayId -> { - if (dialog == null) dialog = PayIdDialog(requireContext()).apply { - this.show() - } - (dialog as? PayIdDialog)?.stopProgress() - } - CreatingPayIdState.Waiting -> (dialog as? PayIdDialog)?.showProgress() - } - } - is WalletDialog.SelectAmountToSendDialog -> { - if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply { - this.show(walletDialog.amounts) - } - } - is WalletDialog.WarningDialog -> { - if (dialog == null) dialog = WarningDialog(requireContext()).apply { - this.show(walletDialog.type) - } - } - null -> { - dialog?.dismiss() - dialog = null - } - } - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.details_menu -> { store.state.globalState.scanNoteResponse?.let { scanNoteResponse -> store.dispatch(DetailsAction.PrepareScreen( scanNoteResponse.card, scanNoteResponse, - store.state.walletState.wallet, + store.state.walletState.walletManagers.map { it.wallet }, store.state.globalState.configManager?.config?.isCreatingTwinCardsAllowed, CardTou(), store.state.globalState.appCurrency @@ -326,41 +173,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber R.id.chip_legacy - is BitcoinAddressType.Segwit -> R.id.chip_default - is CardanoAddressType.Byron -> R.id.chip_legacy - is CardanoAddressType.Shelley -> R.id.chip_default - else -> View.NO_ID - } - } - - fun idToType(id: Int, blockchain: Blockchain?): AddressType? { - return when (id) { - R.id.chip_default -> { - when (blockchain) { - Blockchain.Bitcoin -> BitcoinAddressType.Segwit - Blockchain.CardanoShelley -> CardanoAddressType.Shelley - else -> null - } - } - R.id.chip_legacy -> { - when (blockchain) { - Blockchain.Bitcoin -> BitcoinAddressType.Legacy - Blockchain.CardanoShelley -> CardanoAddressType.Byron - else -> null - } - } - else -> null - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/PendingTransactionsAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/features/wallet/ui/PendingTransactionsAdapter.kt rename to app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt index 93e0202458..09c6094d38 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/PendingTransactionsAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.wallet.ui +package com.tangem.tap.features.wallet.ui.adapters import android.view.LayoutInflater import android.view.View diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt new file mode 100644 index 0000000000..c14d364abf --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt @@ -0,0 +1,136 @@ +package com.tangem.tap.features.wallet.ui.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +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.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 +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.item_currency_wallet.view.* +import java.math.BigDecimal + +class WalletAdapter + : ListAdapter(DiffUtilCallback) { + + override fun getItemId(position: Int): Long { + return currentList[position].currencyData.currencySymbol?.hashCode()?.toLong() ?: 0 + } + + fun submitList(list: List, primaryBlockchain: Blockchain?, primaryToken: Token? = null) { + val listModified = list.toMutableList() + val primaryBlockchainWallet = when ( + val index = listModified.indexOfFirst { it.blockchain == primaryBlockchain } + ) { + -1 -> null + else -> listModified.removeAt(index) + } + + + val primaryTokenWallet = if (primaryToken == null) null else when ( + val index = listModified.indexOfFirst { it.token == primaryToken } + ) { + -1 -> null + else -> listModified.removeAt(index) + } + + if (list.all { it.currencyData.fiatAmount == null }) { + val sortedList = listOfNotNull(primaryBlockchainWallet, primaryTokenWallet) + listModified + super.submitList(sortedList) + return + } + + val sorted = listModified.sortedWith( + compareByDescending { it.currencyData.fiatAmountRaw ?: BigDecimal.ZERO } + .thenBy { it.currencyData.currencySymbol } + ) + val sortedList = listOfNotNull(primaryBlockchainWallet, primaryTokenWallet) + sorted + super.submitList(sortedList) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WalletsViewHolder { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.item_currency_wallet, parent, false) + return WalletsViewHolder(layout) + } + + override fun onBindViewHolder(holder: WalletsViewHolder, position: Int) { + holder.bind(currentList[position]) + } + + object DiffUtilCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame( + oldItem: WalletData, newItem: WalletData + ) = oldItem == newItem + + override fun areItemsTheSame( + oldItem: WalletData, newItem: WalletData + ) = oldItem == newItem + } + + class WalletsViewHolder(val view: View) : + RecyclerView.ViewHolder(view) { + + fun bind(wallet: WalletData) { + view.tv_currency.text = wallet.currencyData.currency + 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 { + store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet)) + } + val blockchain = wallet.currencyData.currencySymbol?.let { Blockchain.fromCurrency(it) } + if (blockchain != null && blockchain != Blockchain.Unknown) { + view.tv_token_letter.text = null + view.iv_currency.colorFilter = null + view.iv_currency.setImageResource(blockchain.getIconRes()) + } else { + view.tv_token_letter.text = wallet.currencyData.currencySymbol?.take(1) + wallet.token?.getColor()?.let { view.iv_currency.setColorFilter(it) } + view.iv_currency.setImageResource(R.drawable.shape_circle) + } + when (wallet.currencyData.status) { + BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress -> hideWarning() + BalanceStatus.Loading -> { + hideWarning() + if (wallet.currencyData.amount == null) { + view.tv_exchange_rate.text = view.context.getText(R.string.wallet_balance_loading) + } + } + BalanceStatus.TransactionInProgress -> + showWarning(view.context.getString(R.string.wallet_balance_tx_in_progress)) + BalanceStatus.Unreachable -> + showWarning(view.context.getString(R.string.wallet_balance_blockchain_unreachable)) + + BalanceStatus.NoAccount -> + showWarning(view.context.getString(R.string.wallet_error_no_account)) + else -> { + } + } + } + + private fun showWarning(message: String) { + toggleWarning(true) + view.tv_status_error_message.text = message + } + + private fun hideWarning() { + toggleWarning(false) + } + + private fun toggleWarning(show: Boolean) { + view.tv_exchange_rate.show(!show) + view.tv_status_error_message.show(show) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt new file mode 100644 index 0000000000..a15a4e3e9a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt @@ -0,0 +1,133 @@ +package com.tangem.tap.features.wallet.ui.adapters + +import android.graphics.Rect +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration +import com.google.android.play.core.review.ReviewManagerFactory +import com.tangem.tap.common.analytics.AnalyticsEvent +import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler +import com.tangem.tap.common.extensions.getActivity +import com.tangem.tap.common.extensions.getString +import com.tangem.tap.common.extensions.hide +import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.configurable.warningMessage.WarningMessage +import com.tangem.tap.features.feedback.RateCanBeBetterEmail +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.layout_warning.view.* +import timber.log.Timber + +class WarningMessagesAdapter : ListAdapter(DiffUtilCallback) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WarningMessageVH { + val inflater = LayoutInflater.from(parent.context) + val layout = inflater.inflate(R.layout.layout_warning, parent, false) + return WarningMessageVH(layout) + } + + override fun onBindViewHolder(holder: WarningMessageVH, position: Int) { + holder.bind(currentList[position]) + } + + object DiffUtilCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: WarningMessage, newItem: WarningMessage) = oldItem == newItem + + override fun areItemsTheSame(oldItem: WarningMessage, newItem: WarningMessage) = oldItem == newItem + } +} + +class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) { + + fun bind(warning: WarningMessage) { + setBgColor(warning.priority) + setText(warning) + setupControlButtons(warning) + } + + private fun setText(warning: WarningMessage) { + fun getString(resId: Int?, default: String) = if (resId == null) default else view.getString(resId) + + view.tv_title.text = getString(warning.titleResId, warning.title) + view.tv_message.text = getString(warning.messageResId, warning.message) + } + + private fun setBgColor(priority: WarningMessage.Priority) { + val color = when (priority) { + WarningMessage.Priority.Info -> R.color.warning_info + WarningMessage.Priority.Warning -> R.color.warning_warning + WarningMessage.Priority.Critical -> R.color.warning_critical + } + view.card_view.setCardBackgroundColor(view.context.resources.getColor(color)) + } + + private fun setupControlButtons(warning: WarningMessage) { + when (warning.type) { + WarningMessage.Type.Permanent -> { + view.group_controls_temporary.hide() + view.group_controls_rating.hide() + } + WarningMessage.Type.Temporary -> { + view.group_controls_rating.hide() + view.group_controls_temporary.show() + view.btn_got_it.setOnClickListener { store.dispatch(GlobalAction.HideWarningMessage(warning)) } + } + WarningMessage.Type.AppRating -> { + view.group_controls_temporary.hide() + view.group_controls_rating.show() + view.btn_close.setOnClickListener { + FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_DISMISS) + store.dispatch(GlobalAction.HideWarningMessage(warning)) + store.dispatch(WalletAction.Warnings.AppRating.RemindLater) + } + view.btn_can_be_better.setOnClickListener { + FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE) + store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) + store.dispatch(GlobalAction.HideWarningMessage(warning)) + store.dispatch(GlobalAction.SendFeedback(RateCanBeBetterEmail())) + } + view.btn_really_cool.setOnClickListener { + val activity = view.context.getActivity() ?: return@setOnClickListener + + FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_POSITIVE) + store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) + val reviewManager = ReviewManagerFactory.create(activity) + val task = reviewManager.requestReviewFlow() + task.addOnCompleteListener { + if (it.isSuccessful) { + val reviewFlow = reviewManager.launchReviewFlow(activity, it.result) + reviewFlow.addOnCompleteListener { + if (it.isSuccessful) { + // send review was succeed + } else { + // send fails + } + } + } else { + Timber.e(task.exception) + } + }.addOnFailureListener { + Timber.e(it) + } + store.dispatch(GlobalAction.HideWarningMessage(warning)) + } + } + } + } +} + +class SpacesItemDecoration(private val spacePx: Int) : ItemDecoration() { + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + outRect.left = spacePx + outRect.right = spacePx + + outRect.top = spacePx / 2 + outRect.top = spacePx / 2 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/PayIdDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/PayIdDialog.kt deleted file mode 100644 index dd1944ac74..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/PayIdDialog.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.tap.features.wallet.ui.dialogs - -import android.app.Dialog -import android.content.Context -import android.view.View -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.extensions.show -import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.notificationsHandler -import com.tangem.tap.store -import com.tangem.wallet.R -import kotlinx.android.synthetic.main.dialog_create_payid.* - -class PayIdDialog(context: Context) : Dialog(context) { - - init { - this.setContentView(R.layout.dialog_create_payid) - } - - override fun show() { - notificationsHandler?.replaceBaseLayout(this.cl_payid_dialog) - this.setOnDismissListener { - notificationsHandler?.returnBaseLayout() - store.dispatch(WalletAction.CreatePayId.Cancel) - } - this.btn_create_payid?.setOnClickListener { - if (this.et_payid.text.isNullOrBlank()) { - store.dispatch(WalletAction.CreatePayId.EmptyField) - } else { - val payid = this.et_payid.text!!.toString() + - this.context.getString(R.string.wallet_create_payid_domain) - store.dispatch(WalletAction.CreatePayId.CompleteCreatingPayId(payid)) - } - } - super.show() - } - - fun showProgress() { - this.btn_create_payid?.visibility = View.INVISIBLE - this.pb_create_payid?.show() - } - - fun stopProgress() { - this.btn_create_payid?.show() - this.pb_create_payid?.hide() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt new file mode 100644 index 0000000000..b3f63a0236 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt @@ -0,0 +1,34 @@ +package com.tangem.tap.features.wallet.ui.dialogs + +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 + +/** +[REDACTED_AUTHOR] + */ +class ScanFailsDialog { + + companion object { + fun create(context: Context): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.common_warning)) + setMessage(R.string.alert_troubleshooting_scan_card_title) + setPositiveButton(R.string.alert_button_request_support) { _, _ -> + store.dispatch(GlobalAction.SendFeedback(ScanFailsEmail())) + } + setNegativeButton(R.string.common_cancel) { _, _ -> } + setOnDismissListener { + store.dispatch(HomeAction.HideDialog) + store.dispatch(WalletAction.HideDialog) + } + }.create() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/WarningDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/WarningDialog.kt deleted file mode 100644 index 6f70a4f60d..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/WarningDialog.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.tap.features.wallet.ui.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.features.wallet.redux.WarningType -import com.tangem.tap.store -import com.tangem.wallet.R - -class WarningDialog(context: Context) : AlertDialog(context) { - - private val dialog: AlertDialog = Builder(context) - .setTitle(context.getString(R.string.common_warning)) - .setPositiveButton(context.getString(R.string.common_ok)) { _, _ -> - dismiss() - }.setOnDismissListener { - store.dispatch(WalletAction.SaveCardId) - store.dispatch(WalletAction.HideDialog) - } - .create() - - fun show(warningType: WarningType) { - val messageRes = when (warningType) { - WarningType.CardSignedHashesBefore -> R.string.alert_card_signed_transactions - WarningType.DevCard -> R.string.alert_developer_card - } - dialog.setMessage(dialog.context.getString(messageRes)) - dialog.show() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt new file mode 100644 index 0000000000..9e2320d30c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -0,0 +1,148 @@ +package com.tangem.tap.features.wallet.ui.wallet + +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 +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletDialog +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.features.wallet.ui.WalletFragment +import com.tangem.tap.features.wallet.ui.adapters.WalletAdapter +import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.card_balance.* +import kotlinx.android.synthetic.main.fragment_wallet.* +import kotlinx.android.synthetic.main.layout_balance_error.* +import kotlinx.android.synthetic.main.layout_wallet_long_buttons.* + + +class MultiWalletView : WalletView { + + private var fragment: WalletFragment? = null + private var dialog: Dialog? = null + + private lateinit var walletsAdapter: WalletAdapter + + + override fun changeWalletView(fragment: WalletFragment) { + setFragment(fragment) + onViewCreated() + showMultiWalletView() + setupButtons() + } + + + private fun showMultiWalletView() { + val fragment = fragment ?: return + fragment.tv_twin_card_number.hide() + fragment.iv_twin_card.hide() + fragment.rv_pending_transaction.hide() + fragment.l_card_balance.hide() + fragment.l_address.hide() + fragment.l_buttons_short.hide() + fragment.l_buttons_long.hide() + fragment.btn_scan_multiwallet?.show() + fragment.rv_multiwallet.show() + fragment.btn_add_token.show() + } + + private fun setupButtons() { + val fragment = fragment ?: return + fragment.btn_scan_multiwallet?.setOnClickListener { store.dispatch(WalletAction.Scan) } + } + + override fun setFragment(fragment: WalletFragment) { + this.fragment = fragment + } + + override fun removeFragment() { + this.fragment = null + } + + override fun onViewCreated() { + setupWalletsRecyclerView() + } + + private fun setupWalletsRecyclerView() { + val fragment = fragment ?: return + walletsAdapter = WalletAdapter() + walletsAdapter.setHasStableIds(true) + fragment.rv_multiwallet.layoutManager = LinearLayoutManager(fragment.requireContext()) + fragment.rv_multiwallet.adapter = walletsAdapter + } + + override fun onNewState(state: WalletState) { + val fragment = fragment ?: return + + walletsAdapter.submitList(state.wallets, state.primaryBlockchain, state.primaryToken) + + fragment.btn_add_token?.setOnClickListener { + store.dispatch(TokensAction.LoadCurrencies) + store.dispatch(TokensAction.SetAddedCurrencies(state.wallets)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens)) + } + handleErrorStates(state, fragment) + handleDialogs(state.walletDialog) + } + + private fun handleErrorStates(state: WalletState, fragment: WalletFragment) { + when (state.primaryWallet?.currencyData?.status) { + BalanceStatus.EmptyCard -> { + showErrorState( + fragment, + fragment.getText(R.string.wallet_error_empty_card), + fragment.getString(R.string.wallet_error_empty_card_subtitle) + ) + configureButtonsForEmptyWalletState(fragment) + } + BalanceStatus.UnknownBlockchain -> { + showErrorState( + fragment, + fragment.getText(R.string.wallet_error_unsupported_blockchain), + fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle) + ) + } + } + } + + private fun showErrorState( + fragment: WalletFragment, errorTitle: CharSequence, errorDescription: CharSequence, + ) { + fragment.l_card_balance.show() + fragment.l_balance.hide() + fragment.l_balance_error.show() + fragment.rv_multiwallet.hide() + fragment.btn_add_token.hide() + fragment.tv_error_title.text = errorTitle + fragment.tv_error_descriptions.text = errorDescription + } + + private fun configureButtonsForEmptyWalletState(fragment: WalletFragment) { + fragment.btn_scan_multiwallet.hide() + fragment.l_buttons_long.show() + fragment.btn_scan_long.setOnClickListener { store.dispatch(WalletAction.Scan) } + fragment.btn_confirm_long.setOnClickListener { store.dispatch(WalletAction.CreateWallet) } + fragment.btn_confirm_long.text = fragment.getText(R.string.wallet_button_create_wallet) + } + + 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 { show() } + } + else -> { + dialog?.dismiss() + dialog = null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt new file mode 100644 index 0000000000..d6762f815b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -0,0 +1,247 @@ +package com.tangem.tap.features.wallet.ui.wallet + +import android.app.Dialog +import android.view.View +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.* +import com.tangem.tap.features.wallet.ui.BalanceWidget +import com.tangem.tap.features.wallet.ui.MultipleAddressUiHelper +import com.tangem.tap.features.wallet.ui.WalletFragment +import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter +import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog +import com.tangem.tap.features.wallet.ui.dialogs.QrDialog +import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.card_balance.* +import kotlinx.android.synthetic.main.fragment_wallet.* +import kotlinx.android.synthetic.main.layout_address.* +import kotlinx.android.synthetic.main.layout_wallet_long_buttons.* +import kotlinx.android.synthetic.main.layout_wallet_short_buttons.* + +class SingleWalletView : WalletView { + + private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter + private var fragment: WalletFragment? = null + private var dialog: Dialog? = null + + override fun setFragment(fragment: WalletFragment) { + this.fragment = fragment + } + + override fun removeFragment() { + fragment = null + } + + override fun changeWalletView(fragment: WalletFragment) { + setFragment(fragment) + onViewCreated() + showSingleWalletView() + } + + private fun showSingleWalletView() { + val fragment = fragment ?: return + fragment.rv_multiwallet.hide() + fragment.btn_add_token.hide() + fragment.btn_scan_multiwallet?.hide() + fragment.rv_pending_transaction.hide() + fragment.l_card_balance.show() + fragment.l_address.show() + } + + override fun onViewCreated() { + setupTransactionsRecyclerView() + } + + + private fun setupTransactionsRecyclerView() { + val fragment = fragment ?: return + pendingTransactionAdapter = PendingTransactionsAdapter() + fragment.rv_pending_transaction.layoutManager = LinearLayoutManager(fragment.requireContext()) + fragment.rv_pending_transaction.adapter = pendingTransactionAdapter + } + + override fun onNewState(state: WalletState) { + setupTwinCards(state.twinCardsState) + state.primaryWallet ?: return + setupButtons(state.primaryWallet, state.twinCardsState != null) + setupAddressCard(state.primaryWallet) + showPendingTransactionsIfPresent(state.primaryWallet.pendingTransactions) + setupBalance(state, state.primaryWallet) + handleDialogs(state.walletDialog) + } + + private fun showPendingTransactionsIfPresent(pendingTransactions: List) { + pendingTransactionAdapter.submitList(pendingTransactions) + fragment?.rv_pending_transaction?.show(pendingTransactions.isNotEmpty()) + } + + + private fun setupBalance(state: WalletState, primaryWallet: WalletData) { + fragment?.apply { + this.l_balance.show() + BalanceWidget(this, primaryWallet.currencyData, state.twinCardsState != null).setup() + } + } + + private fun setupTwinCards(twinCardsState: TwinCardsState?) { + fragment?.apply { + twinCardsState?.cardNumber?.let { cardNumber -> + this.tv_twin_card_number.show() + this.iv_twin_card.show() + val number = when (cardNumber) { + TwinCardNumber.First -> "1" + TwinCardNumber.Second -> "2" + } + this.tv_twin_card_number.text = + this.getString(R.string.wallet_twins_chip_format, number) + } + if (twinCardsState?.cardNumber == null) { + this.tv_twin_card_number.hide() + this.iv_twin_card.hide() + } + if (twinCardsState?.showTwinOnboarding == true) { + store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding)) + } + } + + } + + private fun setupButtons(state: WalletData, isTwinsWallet: Boolean) { + val fragment = fragment ?: return + + setupButtonsType(state, fragment) + + val btnConfirm = if (state.topUpState.allowed) { + fragment.btn_confirm_short + } else { + fragment.btn_confirm_long + } + val btnScan = if (state.topUpState.allowed) { + fragment.btn_scan_short + } else { + fragment.btn_scan_long + } + + setupConfirmButton(state, btnConfirm, fragment, isTwinsWallet) + + btnScan.setOnClickListener { + store.dispatch(WalletAction.Scan) + } + + fragment.btn_copy.setOnClickListener { + state.walletAddresses?.selectedAddress?.address?.let { addressString -> + store.dispatch(WalletAction.CopyAddress(addressString, fragment.requireContext())) + } + } + fragment.btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) } + + fragment.btn_top_up.setOnClickListener { + store.dispatch( + WalletAction.TopUpAction.TopUp(fragment.requireContext(), R.color.backgroundLightGray) + ) + } + } + + private fun setupButtonsType(state: WalletData, fragment: WalletFragment) { + if (state.topUpState.allowed) { + fragment.l_buttons_long.hide() + fragment.l_buttons_short.show() + } else { + fragment.l_buttons_long.show() + fragment.l_buttons_short.hide() + } + } + + private fun setupConfirmButton( + state: WalletData, btnConfirm: Button, fragment: WalletFragment, isTwinsWallet: Boolean, + ) { + val buttonTitle = when (state.mainButton) { + is WalletMainButton.SendButton -> R.string.wallet_button_send + is WalletMainButton.CreateWalletButton -> { + if (!isTwinsWallet) { + R.string.wallet_button_create_wallet + } else { + R.string.wallet_button_create_twin_wallet + } + } + } + btnConfirm.text = fragment.getString(buttonTitle) + btnConfirm.isEnabled = state.mainButton.enabled + btnConfirm.setOnClickListener { + when (state.mainButton) { + is WalletMainButton.SendButton -> store.dispatch(WalletAction.Send()) + is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet) + } + } + } + + + private fun setupAddressCard(state: WalletData) { + val fragment = fragment ?: return + if (state.walletAddresses != null && state.blockchain != null) { + fragment.l_address?.show() + if (state.shouldShowMultipleAddress()) { + (fragment.l_address as? ViewGroup)?.beginDelayedTransition() + fragment.chip_group_address_type.show() + fragment.chip_group_address_type.fitChipsByGroupWidth() + + val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) + if (checkedId != View.NO_ID) fragment.chip_group_address_type.check(checkedId) + + fragment.chip_group_address_type.setOnCheckedChangeListener { group, checkedId -> + if (checkedId == -1) return@setOnCheckedChangeListener + val type = MultipleAddressUiHelper.idToType(checkedId, state.blockchain) + type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) } + } + } else { + fragment.chip_group_address_type.hide() + } + fragment.tv_address.text = state.walletAddresses.selectedAddress.address + fragment.tv_explore?.setOnClickListener { + store.dispatch(WalletAction.ExploreAddress( + state.walletAddresses.selectedAddress.exploreUrl, + fragment.requireContext())) + } + } else { + fragment.l_address?.hide() + } + } + + private fun handleDialogs(walletDialog: StateDialog?) { + val fragment = fragment ?: return + val context = fragment.context ?: return + when (walletDialog) { + is WalletDialog.QrDialog -> { + if (walletDialog.qrCode != null && walletDialog.shareUrl != null) { + if (dialog == null) dialog = QrDialog(context).apply { + this.showQr( + walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName + ) + } + } + } + is WalletDialog.SelectAmountToSendDialog -> { + if (dialog == null) dialog = AmountToSendDialog(context).apply { + this.show(walletDialog.amounts) + } + } + is WalletDialog.ScanFailsDialog -> { + if (dialog == null) dialog = ScanFailsDialog.create(context).apply { show() } + } + null -> { + dialog?.dismiss() + dialog = null + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt new file mode 100644 index 0000000000..ab49865c02 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.features.wallet.ui.wallet + +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.WalletFragment + +interface WalletView { + + fun setFragment(fragment: WalletFragment) + + fun removeFragment() + + fun changeWalletView(fragment: WalletFragment) + + fun onViewCreated() + + fun onNewState(state: WalletState) + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt index 08346630f3..871e3eb3d5 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -11,16 +11,18 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.network.coinmarketcap.FiatCurrency +import java.util.* class PreferencesStorage(applicationContext: Application) { - private val preferences: SharedPreferences by lazy { - applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) - } + private val preferences: SharedPreferences = applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + + val appRatingLaunchObserver: AppRatingLaunchObserver init { incrementLaunchCounter() + appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches()) } private val fiatCurrenciesAdapter: JsonAdapter> by lazy { @@ -97,4 +99,63 @@ class PreferencesStorage(applicationContext: Application) { private const val APP_LAUNCH_COUNT_KEY = "launchCount" } +} + +class AppRatingLaunchObserver( + private val preferences: SharedPreferences, + private val launchCounts: Int, +) { + private val K_SHOW_RATING_AT_LAUNCH_COUNT = "showRatingDialogAtLaunchCount" + private val K_FUNDS_FOUND_DATE = "fundsFoundDate" + private val K_USER_WAS_INTERACT_WITH_RATING = "userWasInteractWithRating" + + private val deferShowing = 20 + private val firstShowing = 3 + private var fundsFoundDate: Calendar? = null + + init { + val dateTimeMs = preferences.getLong(K_FUNDS_FOUND_DATE, -1) + if (dateTimeMs > 0) fundsFoundDate = Calendar.getInstance().apply { timeInMillis = dateTimeMs } + } + + fun foundWalletWithFunds() { + if (fundsFoundDate != null) return + + fundsFoundDate = Calendar.getInstance() + preferences.edit(true) { + putLong(K_FUNDS_FOUND_DATE, fundsFoundDate!!.timeInMillis).apply() + putInt(K_SHOW_RATING_AT_LAUNCH_COUNT, launchCounts + firstShowing) + } + } + + fun isReadyToShow(): Boolean { + val fundsDate = fundsFoundDate ?: return false + + if (!userWasInteractWithRating()) { + val diff = Calendar.getInstance().timeInMillis - fundsDate.timeInMillis + val diffInDays = diff / (1000 * 60 * 60 * 24) + return launchCounts >= getCounterOfNextShowing() && diffInDays >= firstShowing + } + + val nextShowing = getCounterOfNextShowing() + return launchCounts >= nextShowing + } + + fun applyDelayedShowing() { + updateNextShowing(launchCounts + deferShowing) + } + + fun setNeverToShow() { + updateNextShowing(999999999) + } + + private fun updateNextShowing(at: Int) { + val editor = preferences.edit() + editor.putInt(K_SHOW_RATING_AT_LAUNCH_COUNT, at) + editor.putBoolean(K_USER_WAS_INTERACT_WITH_RATING, true) + editor.apply() + } + + private fun userWasInteractWithRating(): Boolean = preferences.getBoolean(K_USER_WAS_INTERACT_WITH_RATING, false) + private fun getCounterOfNextShowing(): Int = preferences.getInt(K_SHOW_RATING_AT_LAUNCH_COUNT, firstShowing) } \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_binance.png b/app/src/main/res/drawable-hdpi/ic_binance.png new file mode 100644 index 0000000000..edce12fa14 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_binance.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_btc.png b/app/src/main/res/drawable-hdpi/ic_btc.png new file mode 100644 index 0000000000..be16f5a51b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_btc.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_btc_cash.png b/app/src/main/res/drawable-hdpi/ic_btc_cash.png new file mode 100644 index 0000000000..ec0864fdb8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_btc_cash.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_cardano.png b/app/src/main/res/drawable-hdpi/ic_cardano.png new file mode 100644 index 0000000000..8a4ac01ad4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_cardano.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_eth.png b/app/src/main/res/drawable-hdpi/ic_eth.png new file mode 100644 index 0000000000..9db27f814e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_eth.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_ltc.png b/app/src/main/res/drawable-hdpi/ic_ltc.png new file mode 100644 index 0000000000..6c0fb4ad3c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_ltc.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_rsk.png b/app/src/main/res/drawable-hdpi/ic_rsk.png new file mode 100644 index 0000000000..be49cda24f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_rsk.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_stellar.png b/app/src/main/res/drawable-hdpi/ic_stellar.png new file mode 100644 index 0000000000..7bb500ef1d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stellar.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_tezos.png b/app/src/main/res/drawable-hdpi/ic_tezos.png new file mode 100644 index 0000000000..938fc9d3d4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_tezos.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_xlm.png b/app/src/main/res/drawable-hdpi/ic_xlm.png new file mode 100644 index 0000000000..6155f291ea Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_xlm.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_xrp.png b/app/src/main/res/drawable-hdpi/ic_xrp.png new file mode 100644 index 0000000000..058650f44b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_xrp.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_binance.png b/app/src/main/res/drawable-ldpi/ic_binance.png new file mode 100644 index 0000000000..18aa82ea4b Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_binance.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_btc.png b/app/src/main/res/drawable-ldpi/ic_btc.png new file mode 100644 index 0000000000..982bb50ad1 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_btc.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_btc_cash.png b/app/src/main/res/drawable-ldpi/ic_btc_cash.png new file mode 100644 index 0000000000..3789ae5071 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_btc_cash.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_cardano.png b/app/src/main/res/drawable-ldpi/ic_cardano.png new file mode 100644 index 0000000000..dd8332ef62 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_cardano.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_eth.png b/app/src/main/res/drawable-ldpi/ic_eth.png new file mode 100644 index 0000000000..b8cd7b26ab Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_eth.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_ltc.png b/app/src/main/res/drawable-ldpi/ic_ltc.png new file mode 100644 index 0000000000..a9dc421e25 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_ltc.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_rsk.png b/app/src/main/res/drawable-ldpi/ic_rsk.png new file mode 100644 index 0000000000..416da64bb7 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_rsk.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_stellar.png b/app/src/main/res/drawable-ldpi/ic_stellar.png new file mode 100644 index 0000000000..0de7200a90 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_stellar.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_tezos.png b/app/src/main/res/drawable-ldpi/ic_tezos.png new file mode 100644 index 0000000000..502e81b732 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_tezos.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_xlm.png b/app/src/main/res/drawable-ldpi/ic_xlm.png new file mode 100644 index 0000000000..3262fe8e13 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_xlm.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_xrp.png b/app/src/main/res/drawable-ldpi/ic_xrp.png new file mode 100644 index 0000000000..70d484bc18 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_xrp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_binance.png b/app/src/main/res/drawable-mdpi/ic_binance.png new file mode 100644 index 0000000000..9254cb4a01 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_binance.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_btc.png b/app/src/main/res/drawable-mdpi/ic_btc.png new file mode 100644 index 0000000000..f02a7a13a1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_btc.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_btc_cash.png b/app/src/main/res/drawable-mdpi/ic_btc_cash.png new file mode 100644 index 0000000000..e0dea3ea53 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_btc_cash.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_cardano.png b/app/src/main/res/drawable-mdpi/ic_cardano.png new file mode 100644 index 0000000000..7ff6fc7ec0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_cardano.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_eth.png b/app/src/main/res/drawable-mdpi/ic_eth.png new file mode 100644 index 0000000000..e01e76371f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_eth.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_ltc.png b/app/src/main/res/drawable-mdpi/ic_ltc.png new file mode 100644 index 0000000000..13f1de9253 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_ltc.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_rsk.png b/app/src/main/res/drawable-mdpi/ic_rsk.png new file mode 100644 index 0000000000..af269e0bda Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_rsk.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stellar.png b/app/src/main/res/drawable-mdpi/ic_stellar.png new file mode 100644 index 0000000000..207f7c21a8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stellar.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_tezos.png b/app/src/main/res/drawable-mdpi/ic_tezos.png new file mode 100644 index 0000000000..71d699414b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_tezos.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_xlm.png b/app/src/main/res/drawable-mdpi/ic_xlm.png new file mode 100644 index 0000000000..1a35164ed1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_xlm.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_xrp.png b/app/src/main/res/drawable-mdpi/ic_xrp.png new file mode 100644 index 0000000000..ec5e3bb680 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_xrp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_binance.png b/app/src/main/res/drawable-xhdpi/ic_binance.png new file mode 100644 index 0000000000..4a1619f750 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_binance.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_btc.png b/app/src/main/res/drawable-xhdpi/ic_btc.png new file mode 100644 index 0000000000..e58ee8b721 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_btc.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_btc_cash.png b/app/src/main/res/drawable-xhdpi/ic_btc_cash.png new file mode 100644 index 0000000000..4709218591 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_btc_cash.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_cardano.png b/app/src/main/res/drawable-xhdpi/ic_cardano.png new file mode 100644 index 0000000000..5e9c1be96a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_cardano.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_eth.png b/app/src/main/res/drawable-xhdpi/ic_eth.png new file mode 100644 index 0000000000..5e5807bf8b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_eth.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_ltc.png b/app/src/main/res/drawable-xhdpi/ic_ltc.png new file mode 100644 index 0000000000..1135186fa5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_ltc.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_rsk.png b/app/src/main/res/drawable-xhdpi/ic_rsk.png new file mode 100644 index 0000000000..2016162c6d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_rsk.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stellar.png b/app/src/main/res/drawable-xhdpi/ic_stellar.png new file mode 100644 index 0000000000..d6073f7fc7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stellar.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_tezos.png b/app/src/main/res/drawable-xhdpi/ic_tezos.png new file mode 100644 index 0000000000..811d221bdb Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_tezos.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_xlm.png b/app/src/main/res/drawable-xhdpi/ic_xlm.png new file mode 100644 index 0000000000..7ebe6a2fe8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_xlm.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_xrp.png b/app/src/main/res/drawable-xhdpi/ic_xrp.png new file mode 100644 index 0000000000..aa8adabc2c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_xrp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_binance.png b/app/src/main/res/drawable-xxhdpi/ic_binance.png new file mode 100644 index 0000000000..dbe9c0805f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_binance.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_btc.png b/app/src/main/res/drawable-xxhdpi/ic_btc.png new file mode 100644 index 0000000000..87f552a566 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_btc.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_btc_cash.png b/app/src/main/res/drawable-xxhdpi/ic_btc_cash.png new file mode 100644 index 0000000000..11d710501e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_btc_cash.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_cardano.png b/app/src/main/res/drawable-xxhdpi/ic_cardano.png new file mode 100644 index 0000000000..74faea419a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_cardano.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_eth.png b/app/src/main/res/drawable-xxhdpi/ic_eth.png new file mode 100644 index 0000000000..1e7a0e7952 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_eth.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_ltc.png b/app/src/main/res/drawable-xxhdpi/ic_ltc.png new file mode 100644 index 0000000000..0cf80c3570 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_ltc.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_rsk.png b/app/src/main/res/drawable-xxhdpi/ic_rsk.png new file mode 100644 index 0000000000..3a660c50bc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_rsk.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stellar.png b/app/src/main/res/drawable-xxhdpi/ic_stellar.png new file mode 100644 index 0000000000..8824973627 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stellar.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_tezos.png b/app/src/main/res/drawable-xxhdpi/ic_tezos.png new file mode 100644 index 0000000000..16dccf95c8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_tezos.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_xlm.png b/app/src/main/res/drawable-xxhdpi/ic_xlm.png new file mode 100644 index 0000000000..0dc1b2ba5f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_xlm.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_xrp.png b/app/src/main/res/drawable-xxhdpi/ic_xrp.png new file mode 100644 index 0000000000..881d6b7b4b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_xrp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_binance.png b/app/src/main/res/drawable-xxxhdpi/ic_binance.png new file mode 100644 index 0000000000..f18f7ff9d7 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_binance.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_btc.png b/app/src/main/res/drawable-xxxhdpi/ic_btc.png new file mode 100644 index 0000000000..165164f069 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_btc.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_btc_cash.png b/app/src/main/res/drawable-xxxhdpi/ic_btc_cash.png new file mode 100644 index 0000000000..babd59d8b1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_btc_cash.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_cardano.png b/app/src/main/res/drawable-xxxhdpi/ic_cardano.png new file mode 100644 index 0000000000..1f8b6888a5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_cardano.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_eth.png b/app/src/main/res/drawable-xxxhdpi/ic_eth.png new file mode 100644 index 0000000000..74ead90795 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_eth.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_ltc.png b/app/src/main/res/drawable-xxxhdpi/ic_ltc.png new file mode 100644 index 0000000000..d159e6728f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_ltc.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_rsk.png b/app/src/main/res/drawable-xxxhdpi/ic_rsk.png new file mode 100644 index 0000000000..b442ac65a4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_rsk.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stellar.png b/app/src/main/res/drawable-xxxhdpi/ic_stellar.png new file mode 100644 index 0000000000..a19f0db146 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stellar.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_tezos.png b/app/src/main/res/drawable-xxxhdpi/ic_tezos.png new file mode 100644 index 0000000000..d91ceab8fc Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_tezos.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_xlm.png b/app/src/main/res/drawable-xxxhdpi/ic_xlm.png new file mode 100644 index 0000000000..6500485826 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_xlm.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_xrp.png b/app/src/main/res/drawable-xxxhdpi/ic_xrp.png new file mode 100644 index 0000000000..22400ff291 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_xrp.png differ diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000000..9b975701fe --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_copy_chip.xml b/app/src/main/res/drawable/ic_copy_chip.xml new file mode 100644 index 0000000000..83325e0952 --- /dev/null +++ b/app/src/main/res/drawable/ic_copy_chip.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_ducatus.png b/app/src/main/res/drawable/ic_ducatus.png new file mode 100644 index 0000000000..cfae76f29f Binary files /dev/null and b/app/src/main/res/drawable/ic_ducatus.png differ diff --git a/app/src/main/res/drawable/ic_inactive.xml b/app/src/main/res/drawable/ic_inactive.xml new file mode 100644 index 0000000000..eb19f0ce6d --- /dev/null +++ b/app/src/main/res/drawable/ic_inactive.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_no_tokens.xml b/app/src/main/res/drawable/ic_no_tokens.xml new file mode 100644 index 0000000000..f1149e0fad --- /dev/null +++ b/app/src/main/res/drawable/ic_no_tokens.xml @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000000..d698df7efe --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_share_chip.xml b/app/src/main/res/drawable/ic_share_chip.xml new file mode 100644 index 0000000000..e026b36c2e --- /dev/null +++ b/app/src/main/res/drawable/ic_share_chip.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/shape_circle.xml b/app/src/main/res/drawable/shape_circle.xml new file mode 100644 index 0000000000..f857819f4d --- /dev/null +++ b/app/src/main/res/drawable/shape_circle.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_tokens.xml b/app/src/main/res/layout/fragment_add_tokens.xml new file mode 100644 index 0000000000..3ed5cbd260 --- /dev/null +++ b/app/src/main/res/layout/fragment_add_tokens.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index 2533020f4a..00c0626cba 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -116,15 +116,15 @@ android:id="@+id/tv_disclaimer" android:layout_width="match_parent" android:layout_height="wrap_content" + android:drawablePadding="15dp" android:paddingTop="7dp" android:paddingBottom="10dp" - android:drawablePadding="15dp" + android:text="@string/disclaimer_title" 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_signed_hashes_title" - android:text="@string/disclaimer_title" /> + app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes_title" /> + + + app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" /> + app:layout_constraintTop_toBottomOf="@id/tv_security_title" /> diff --git a/app/src/main/res/layout/fragment_send.xml b/app/src/main/res/layout/fragment_send.xml index 86ec08548b..78131a2e15 100644 --- a/app/src/main/res/layout/fragment_send.xml +++ b/app/src/main/res/layout/fragment_send.xml @@ -74,6 +74,13 @@ android:layout_marginBottom="24dp" tools:visibility="gone" /> + + + android:clipToPadding="false" + android:paddingBottom="33dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/guideline" + app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toBottomOf="@id/iv_card" /> + app:layout_constraintBottom_toBottomOf="@id/iv_twin_card" + app:layout_constraintEnd_toEndOf="@id/iv_twin_card" + app:layout_constraintStart_toStartOf="@id/iv_twin_card" + app:layout_constraintTop_toTopOf="@id/iv_twin_card" + tools:text="Card 2 of 2" /> + app:barrierDirection="bottom" + app:constraint_referenced_ids="iv_card,iv_twin_card" /> + + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" /> @@ -117,22 +132,75 @@ layout="@layout/layout_address" android:layout_width="match_parent" android:layout_height="wrap_content" + android:visibility="gone" app:layout_constraintTop_toBottomOf="@id/l_card_balance" /> + + + + + + + + + android:visibility="gone" + android:layout_marginTop="30dp" + app:layout_constraintTop_toBottomOf="@id/l_address" + app:layout_constraintVertical_bias="1" + app:layout_constraintBottom_toBottomOf="parent" /> + android:visibility="gone" + android:layout_marginTop="30dp" + app:layout_constraintTop_toBottomOf="@id/l_address" + app:layout_constraintVertical_bias="1" + app:layout_constraintBottom_toBottomOf="parent" /> + + diff --git a/app/src/main/res/layout/fragment_wallet_details.xml b/app/src/main/res/layout/fragment_wallet_details.xml new file mode 100644 index 0000000000..757eeab445 --- /dev/null +++ b/app/src/main/res/layout/fragment_wallet_details.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_currency_subtitle.xml b/app/src/main/res/layout/item_currency_subtitle.xml new file mode 100644 index 0000000000..c4ae27800d --- /dev/null +++ b/app/src/main/res/layout/item_currency_subtitle.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_currency_wallet.xml b/app/src/main/res/layout/item_currency_wallet.xml new file mode 100644 index 0000000000..d4a2a6dfd2 --- /dev/null +++ b/app/src/main/res/layout/item_currency_wallet.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_popular_token.xml b/app/src/main/res/layout/item_popular_token.xml new file mode 100644 index 0000000000..8d22ab57b4 --- /dev/null +++ b/app/src/main/res/layout/item_popular_token.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_balance_wallet_details.xml b/app/src/main/res/layout/layout_balance_wallet_details.xml new file mode 100644 index 0000000000..56bf9b2d69 --- /dev/null +++ b/app/src/main/res/layout/layout_balance_wallet_details.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_search_view.xml b/app/src/main/res/layout/layout_search_view.xml new file mode 100644 index 0000000000..65bbfa77d1 --- /dev/null +++ b/app/src/main/res/layout/layout_search_view.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_wallet_details.xml b/app/src/main/res/layout/layout_wallet_details.xml new file mode 100644 index 0000000000..f8ac54763e --- /dev/null +++ b/app/src/main/res/layout/layout_wallet_details.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_wallet_long_buttons.xml b/app/src/main/res/layout/layout_wallet_long_buttons.xml index d32b3ac917..d63cb4681a 100644 --- a/app/src/main/res/layout/layout_wallet_long_buttons.xml +++ b/app/src/main/res/layout/layout_wallet_long_buttons.xml @@ -1,8 +1,8 @@ + android:layout_height="match_parent"> + + app:layout_constraintVertical_bias="1" /> + app:layout_constraintVertical_bias="1" /> + app:layout_constraintVertical_bias="1" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_warning.xml b/app/src/main/res/layout/layout_warning.xml new file mode 100644 index 0000000000..a9a76d4637 --- /dev/null +++ b/app/src/main/res/layout/layout_warning.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + +