Updated on 2026-08-14

This commit is contained in:
Tangem 2021-04-15 13:09:54 +03:00
commit 1d03d6ee86
213 changed files with 7054 additions and 1452 deletions

View file

@ -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'

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">DEBUG</string>
<string name="app_name" translatable="false">DebugTangem</string>
</resources>

View file

@ -81,6 +81,17 @@
</activity>
<activity android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>

View file

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

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

View file

@ -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
}
}

View file

@ -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)))
}
}

View file

@ -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"),
}

View file

@ -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)
}

View file

@ -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) {

View file

@ -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()

View file

@ -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
}
}

View file

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

View file

@ -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()
}
}

View file

@ -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<EncodeHintType, Any>()
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 {

View file

@ -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<EncodeHintType, Any>()
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
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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)

View file

@ -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),
)
}

View file

@ -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,
)
}
}

View file

@ -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<ScanNoteResponse>) : GlobalAction()
object Reset : GlobalAction()
object Increment : GlobalAction()
}
data class SaveScanNoteResponse(val scanNoteResponse: ScanNoteResponse) : GlobalAction()
data class SetFiatRate(
val fiatRates: Pair<CryptoCurrencyName, BigDecimal>
) : 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()
}

View file

@ -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<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
when (action) {
is GlobalAction.ScanFailsCounter.ChooseBehavior -> {
when (action.result) {
is CompletionResult.Success -> store.dispatch(GlobalAction.ScanFailsCounter.Reset)
is CompletionResult.Failure -> {
if (action.result.error is TangemSdkError.UserCancelled) {
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
if (store.state.globalState.scanCardFailsCounter >= 2) {
store.dispatch(HomeAction.ShowDialog.ScanFails)
store.dispatch(WalletAction.ShowDialog.ScanFails)
}
} else {
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
}
}
}
}
is GlobalAction.RestoreAppCurrency -> {
store.dispatch(GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.getAppCurrency()
))
}
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)
}

View file

@ -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
}
}

View file

@ -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<CryptoCurrencyName, BigDecimal>,
) {
fun getRate(cryptoCurrency: CryptoCurrencyName): BigDecimal? {
return rates[cryptoCurrency]
}
}
typealias CryptoCurrencyName = String
typealias FiatCurrencyName = String
interface StateDialog

View file

@ -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
}

View file

@ -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<String?> = 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<SetPayIdResponse> = 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<VerifyPayIdResponse> = withContext(Dispatchers.IO) {
val splitPayId = payId.split("\$")

View file

@ -0,0 +1,13 @@
package com.tangem.tap.domain
class TapBlockchainConfig {
companion object {
val blockchairApiKey: String = "A___0Shpsu4KagE7oSabrw20DfXAqWlT"
val blockcypherTokens: Set<String> = setOf(
"aa8184b0e0894b88a5688e01b3dc1e82",
"56c4ca23c6484c8f8864c32fde4def8d",
"66a8a37c5e9d4d2c9bb191acfe7f93aa"
)
val infuraProjectId: String = "613a0b14833145968b1f656240c7d245"
}
}

View file

@ -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)

View file

@ -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<CryptoCurrencyName>) {
val results = mutableListOf<Pair<CryptoCurrencyName, Result<BigDecimal>?>>()
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<Wallet>, 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<String?>? {
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<String?>) {
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<Pair<CryptoCurrencyName, Result<BigDecimal>?>>) {
withContext(Dispatchers.Main) {
results.map {
when (it.second) {
is Result.Success -> {
val rate = it.first to (it.second as Result.Success<BigDecimal>).data
store.dispatch(GlobalAction.SetFiatRate(rate))
store.dispatch(WalletAction.LoadFiatRate.Success(rate))
}
is Result.Failure -> store.dispatch(WalletAction.LoadFiatRate.Failure)

View file

@ -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"
)
}
}
val Card.isMultiwalletAllowed: Boolean
get() {
return cardData?.productMask?.contains(Product.TwinCard) != true
&& !TapWorkarounds.isStart2Coin
&& this.curve == EllipticCurve.Secp256k1
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.domain.configurable
import com.tangem.wallet.BuildConfig
/**
[REDACTED_AUTHOR]
*/
interface Loader<T> {
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}"
}
}

View file

@ -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<ConfigModel> {
override fun loadConfig(onComplete: (ConfigModel) -> Unit) {
override fun load(onComplete: (ConfigModel) -> Unit) {
val config = try {
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
val valuesAdapter: JsonAdapter<ConfigValueModel> = 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<ConfigModel> {
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)
}
}

View file

@ -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<ConfigModel>,
private val remoteLoader: Loader<ConfigModel>
) {
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"
}
}

View file

@ -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<String>?,
val infuraProjectId: String?,
)
class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) {

View file

@ -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<List<WarningMessage>> {
override fun load(onComplete: (List<WarningMessage>) -> Unit) {
val emptyConfig = listOf<WarningMessage>()
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<List<WarningMessage>> = 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)
}
}

View file

@ -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<Location>,
private val blockchains: List<String>?,
val titleResId: Int? = null,
val messageResId: Int? = null,
val origin: Origin = Origin.Remote,
) {
val blockchainList: List<Blockchain>? 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
}
}

View file

@ -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<WarningMessage> = 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<Blockchain> = emptyList()): List<WarningMessage> {
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
)
}
}

View file

@ -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<ScanNoteResponse> {
@ -53,7 +52,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
}
val walletManager = try {
WalletManagerFactory.makeWalletManager(card)
getWalletManagerFactory().makeWalletManager(card)
} catch (exception: Exception) {
return@run callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
@ -72,8 +71,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
when (verifyResult) {
is CompletionResult.Success -> {
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<ScanNoteRespons
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletManager = try {
WalletManagerFactory.makeMultisigWalletManager(card, twinPublicKey)
getWalletManagerFactory().makeMultisigWalletManager(card, twinPublicKey)
} catch (exception: Exception) {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
return@run
@ -119,5 +117,10 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return null
}
private fun getWalletManagerFactory(): WalletManagerFactory {
val blockchainSdkConfig = store.state.globalState.configManager?.config
?.blockchainSdkConfig ?: BlockchainSdkConfig()
return WalletManagerFactory(blockchainSdkConfig)
}
}
}

View file

@ -0,0 +1,151 @@
package com.tangem.tap.domain.tokens
import android.app.Application
import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Types
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.readJsonFileToString
import com.tangem.tap.network.createMoshi
class CurrenciesRepository(val context: Application) {
private val moshi = createMoshi()
private val tokensAdapter: JsonAdapter<List<TokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, TokenDao::class.java)
)
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = 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<Token> {
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<Token>) {
val json = tokensAdapter.toJson(tokens.map { TokenDao.fromToken(it) })
context.rewriteFile(json, getFileNameForTokens(cardId))
}
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainsAdapter.fromJson(json) ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
private fun saveBlockchains(cardId: String, blockchains: List<Blockchain>) {
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<Token> {
val json = context.assets.readJsonFileToString(POPULAR_TOKENS_FILE_NAME)
return tokensAdapter.fromJson(json)!!.map { it.toToken() }
}
fun getBlockchains(): List<Blockchain> {
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<TokenDao>,
val blockchains: List<Blockchain>
) {
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<Token>,
val blockchains: List<Blockchain>
)

View file

@ -18,7 +18,7 @@ sealed class DetailsAction : Action {
data class PrepareScreen(
val card: Card,
val scanNoteResponse: ScanNoteResponse,
val wallet: Wallet?,
val wallets: List<Wallet>,
val isCreatingTwinWalletAllowed: Boolean?,
val cardTou: CardTou,
val fiatCurrencyName: FiatCurrencyName,

View file

@ -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)
}
}
}
}
}
}

View file

@ -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

View file

@ -13,7 +13,7 @@ import java.util.*
data class DetailsState(
val card: Card? = null,
val wallet: Wallet? = null,
val wallets: List<Wallet> = emptyList(),
val cardInfo: CardInfo? = null,
val appCurrencyState: AppCurrencyState = AppCurrencyState(),
val eraseWalletState: EraseWalletState? = null,

View file

@ -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<Det
tv_signed_hashes.show(state.card?.isTwinCard() != true)
tv_signed_hashes_title.show(state.card?.isTwinCard() != true)
tv_disclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
tv_card_tou.show(state.cardTermsOfUseUrl != null)
@ -115,6 +116,9 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
tv_app_currency_title.setOnClickListener {
store.dispatch(DetailsAction.AppCurrencyAction.ChooseAppCurrency)
}
tv_send_feedback.setOnClickListener {
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
}
tv_security_title.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)

View file

@ -0,0 +1,293 @@
package com.tangem.tap.features.feedback
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.tangem.Log
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.store
import timber.log.Timber
import java.io.File
import java.io.FileWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.*
/**
[REDACTED_AUTHOR]
*/
class FeedbackManager(
val infoHolder: AdditionalEmailInfo,
private val logCollector: TangemLogCollector,
private val email: String = "support@tangem.com",
) {
private lateinit var activity: Activity
fun updateAcivity(activity: Activity) {
this.activity = activity
}
fun send(emailData: EmailData) {
val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null
sendTo(email, emailData.subject, emailData.joinTogether(infoHolder), fileLog)
}
private fun sendTo(email: String, subject: String, message: String, fileLog: File? = null) {
val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
val originalIntentResults = activity.packageManager.queryIntentActivities(emailFilterIntent, 0)
val emailFilterIntentResults = activity.packageManager.queryIntentActivities(emailFilterIntent, 0)
val targetedIntents = originalIntentResults
.filter { originalResult ->
emailFilterIntentResults.any {
originalResult.activityInfo.packageName == it.activityInfo.packageName
}
}
.map {
createEmailShareIntent(email, subject, message, fileLog).apply {
setPackage(it.activityInfo.packageName)
}
}
.toMutableList()
try {
val chooserIntent = Intent.createChooser(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<String>()
override fun log(message: () -> String, level: Log.Level) {
val time = dateFormatter.format(Date())
logs.add("$time: ${message()}\n")
}
fun getLogs(): List<String> = 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<EmailWalletInfo>()
internal var onSendErrorWalletInfo: EmailWalletInfo? = null
var signedHashesCount: String = ""
// device
var phoneModel: String = Build.MODEL
var osVersion: String = Build.VERSION.SDK_INT.toString()
// send error
var destinationAddress: String = ""
var amount: String = ""
var fee: String = ""
var token: String = ""
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<Wallet>) {
walletsInfo.clear()
wallets.forEach { walletsInfo.add(EmailWalletInfo(it.blockchain, getAddress(it), getExploreUri(it))) }
}
fun updateOnSendError(wallet: Wallet, amountToSend: Amount, feeAmount: Amount, destinationAddress: String) {
val amountState = store.state.sendState.amountState
onSendErrorWalletInfo = EmailWalletInfo(wallet.blockchain, getAddress(wallet), getExploreUri(wallet))
this.destinationAddress = destinationAddress
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
if (amountState.typeOfAmount is AmountType.Token) {
token = amountState.amountToExtract?.currencySymbol ?: ""
}
}
private fun getAddress(wallet: Wallet): String {
return if (wallet.addresses.size == 1) {
wallet.address
} else {
val addresses = wallet.addresses.joinToString(", ") {
"${it.type.javaClass.simpleName} - ${it.value}"
}
"Multiple address: $addresses"
}
}
private fun getExploreUri(wallet: Wallet): String {
return if (wallet.addresses.size == 1) {
wallet.getExploreUrl(wallet.address)
} else {
val links = wallet.addresses.joinToString(", ") {
"${it.type.javaClass.simpleName} - ${wallet.getExploreUrl(it.value)}"
}
"Multiple explorers links: $links"
}
}
}
interface EmailData {
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 dont 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 = "Cant 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 = "Cant send a transaction"
override val mainMessage: String = "Please tell us more about your issue. Every small detail can help."
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo()
return StringBuilder().apply {
appendKeyValue("Error", error)
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Blockchain", 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")
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,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<WarningMessage>) : SendAction()
}
}

View file

@ -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<AppState> = { 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<AppState> = { 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))
}

View file

@ -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 {

View file

@ -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<WarningMessage> = listOf(),
val sendButtonState: SendButtonState = SendButtonState.DISABLED,
val dialog: SendAction.Dialog? = null
val dialog: StateDialog? = null
) : SendScreenState {
override val stateId: StateId = StateId.SEND_SCREEN

View file

@ -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 ->

View file

@ -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()
}
}
}

View file

@ -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()

View file

@ -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) {

View file

@ -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<CurrencyListItem>) : TokensAction()
}
object LoadCardTokens : TokensAction() {
data class Success(val tokens: List<Token>) : TokensAction()
}
data class SetAddedCurrencies(val wallets: List<WalletData>) : TokensAction()
}

View file

@ -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<AppState> = { 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)
}
}
}
}

View file

@ -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
}
}

View file

@ -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<TokenWithAmount> = LinkedHashSet(),
val addedCurrencies: List<CryptoCurrencyName> = emptyList(),
val currencies: List<CurrencyListItem> = emptyList(),
) : StateType
data class TokenWithAmount(
val token: Token,
val amount: Amount? = null
)

View file

@ -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<TokensState> {
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<String> = 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) }
}

View file

@ -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<CurrencyListItem, RecyclerView.ViewHolder>(DiffUtilCallback) {
var addedCurrencies: List<CryptoCurrencyName> = emptyList()
private var unfilteredList = listOf<CurrencyListItem>()
fun submitUnfilteredList(list: List<CurrencyListItem>) {
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<CurrencyListItem>() {
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<CurrencyListItem>()
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<CryptoCurrencyName>) {
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<Blockchain>, tokens: List<Token>): List<CurrencyListItem> {
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) }
}
}
}

View file

@ -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<TransactionData>.toPendingTransactions(walletAddress: String): List<Pen
fun List<PendingTransaction>.removeUnknownTransactions(): List<PendingTransaction> {
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<TransactionData>.toPendingTransactionsForToken(token: Token, walletAddress: String): List<PendingTransaction> {
return this.mapNotNull { it.toPendingTransactionForToken(token, walletAddress) }
}

View file

@ -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<WalletManager>) : MultiWallet() {
constructor(walletManager: WalletManager) : this(listOf(walletManager))
}
data class AddBlockchain(val blockchain: Blockchain) : MultiWallet()
data class AddBlockchains(val blockchains: List<Blockchain>) : MultiWallet()
data class AddTokens(val tokens: List<Token>) : MultiWallet()
data class AddToken(val token: Token) : MultiWallet()
data class SaveCurrencies(val cardCurrencies: CardCurrencies) : MultiWallet()
object FindTokensInUse : MultiWallet()
data class FindBlockchainsInUse(val card: Card, val factory: WalletManagerFactory) : MultiWallet()
data class TokenLoaded(val amount: Amount) : MultiWallet()
data class SelectWallet(val walletData: WalletData?) : MultiWallet()
data class RemoveWallet(val walletData: WalletData) : MultiWallet()
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<WarningMessage>) : 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<CryptoCurrencyName, BigDecimal?>) : WalletAction()
data class LoadFiatRate(
val wallet: Wallet? = null, val currency: CryptoCurrencyName? = null
) : WalletAction() {
data class Success(val fiatRate: Pair<CryptoCurrencyName, BigDecimal?>) : 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<Amount>?) : 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()
}
}

View file

@ -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<AppState> = { 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));
}
}
}
}

View file

@ -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<AddressData>()
// 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)
)
}

View file

@ -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<PendingTransaction> = 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<WarningMessage> = mutableListOf(),
val wallets: List<WalletData> = emptyList(),
val walletManagers: List<WalletManager> = 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<Blockchain>
get() = walletManagers.map { it.wallet.blockchain }
fun getWalletManager(currencyName: CryptoCurrencyName?): WalletManager? {
if (currencyName == null) return null
val walletManager = walletManagers.find { it.wallet.blockchain.currency == currencyName }
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<WalletData> {
if (walletData == null) return wallets
return wallets.filter { it.currencyData.currency != walletData.currencyData.currency } + walletData
}
fun replaceSomeWallets(newWallets: List<WalletData>): List<WalletData> {
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<Amount>?) : 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
)
)
data class WalletData(
val pendingTransactions: List<PendingTransaction> = 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
}
}

View file

@ -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))
}
}
}
}
}
}

View file

@ -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));
}
}
}
}

View file

@ -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()
}
}
}
}

View file

@ -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<AppState> = { 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)
}
}

View file

@ -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<WarningMessage> {
val warningManager = store.state.globalState.warningManager ?: return emptyList()
return warningManager.getWarnings(WarningMessage.Location.MainScreen, store.state.walletState.blockchains)
}
}

View file

@ -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
}
}
}

View file

@ -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
)
}
}

View file

@ -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)
)
}
}
}
}

View file

@ -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<AddressData>()
// 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<CryptoCurrencyName, BigDecimal?>,
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
}

View file

@ -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() {

View file

@ -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
}
}
}
}

View file

@ -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<WalletState> {
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<PendingTransaction>) {
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)
}
}
}

View file

@ -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<WalletState> {
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<Walle
oldState.walletState == newState.walletState
}.select { it.walletState }
}
store.dispatch(WalletAction.UpdateWallet)
walletView.setFragment(this)
store.dispatch(WalletAction.UpdateWallet())
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
walletView.removeFragment()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -79,182 +78,69 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
setupTransactionsRecyclerView()
setupWarningsRecyclerView()
walletView.changeWalletView(this)
}
private fun setupTransactionsRecyclerView() {
viewAdapter = PendingTransactionsAdapter()
rv_pending_transaction.layoutManager = LinearLayoutManager(context)
rv_pending_transaction.adapter = viewAdapter
private fun setupWarningsRecyclerView() {
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
}
override fun newState(state: WalletState) {
if (activity == null) return
state.twinCardsState?.cardNumber?.let { cardNumber ->
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<WarningMessage>) {
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<Walle
?.into(iv_card)
}
private fun handleDialogs(walletDialog: WalletDialog?) {
when (walletDialog) {
is WalletDialog.QrDialog -> {
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<Walle
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (store.state.walletState.showDetails) inflater.inflate(R.menu.wallet, menu)
if (store.state.walletState.shouldShowDetails) inflater.inflate(R.menu.wallet, menu)
}
}
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 -> 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
}
}
}
}

View file

@ -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

View file

@ -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<WalletData, WalletAdapter.WalletsViewHolder>(DiffUtilCallback) {
override fun getItemId(position: Int): Long {
return currentList[position].currencyData.currencySymbol?.hashCode()?.toLong() ?: 0
}
fun submitList(list: List<WalletData>, 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<WalletData> { 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<WalletData>() {
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)
}
}
}

View file

@ -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<WarningMessage, WarningMessageVH>(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<WarningMessage>() {
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
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}
}

View file

@ -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
}
}
}
}

View file

@ -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<PendingTransaction>) {
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
}
}
}
}

View file

@ -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)
}

View file

@ -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<List<FiatCurrency>> 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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Some files were not shown because too many files have changed in this diff Show more