Updated on 2026-08-14
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
914
app/src/main/assets/erc20_tokens.json
Normal 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"
|
||||
}
|
||||
]
|
||||
1
app/src/main/assets/tangem-android-config
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 9a731fcb5a58d53c90d7db79e5441bdf18e087fc
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
62
app/src/main/java/com/tangem/tap/common/extensions/String.kt
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
16
app/src/main/java/com/tangem/tap/common/extensions/Token.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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("\$")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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?) {
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 don’t want to share it.\n" +
|
||||
createOptionalMessage(infoHolder)
|
||||
}
|
||||
}
|
||||
|
||||
class RateCanBeBetterEmail : EmailData {
|
||||
override val subject: String = "My suggestions"
|
||||
override val mainMessage: String = "Tell us what functions you are missing, and we will try to help you."
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
|
||||
val walletInfo = infoHolder.walletsInfo[0]
|
||||
return StringBuilder().apply {
|
||||
appendKeyValue("Card ID", infoHolder.cardId)
|
||||
appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
|
||||
appendKeyValue("Phone model", infoHolder.phoneModel)
|
||||
appendKeyValue("OS version", infoHolder.osVersion)
|
||||
appendKeyValue("App version", infoHolder.appVersion)
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
|
||||
class ScanFailsEmail : EmailData {
|
||||
override val subject: String = "Can’t scan a card"
|
||||
override val mainMessage: String = "Please tell us what card do you have?"
|
||||
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
|
||||
return StringBuilder().apply {
|
||||
appendKeyValue("Phone model", infoHolder.phoneModel)
|
||||
appendKeyValue("OS version", infoHolder.osVersion)
|
||||
appendKeyValue("App version", infoHolder.appVersion)
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
|
||||
class SendTransactionFailedEmail(private val error: String) : EmailData {
|
||||
override val subject: String = "Can’t send a transaction"
|
||||
override val mainMessage: String = "Please tell us more about your issue. Every small detail can help."
|
||||
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
|
||||
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo()
|
||||
return StringBuilder().apply {
|
||||
appendKeyValue("Error", error)
|
||||
appendKeyValue("Card ID", infoHolder.cardId)
|
||||
appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
|
||||
appendKeyValue("Token", infoHolder.token)
|
||||
appendKeyValue("Source address", walletInfo.address)
|
||||
appendKeyValue("Destination address", infoHolder.destinationAddress)
|
||||
appendKeyValue("Amount", infoHolder.amount)
|
||||
appendKeyValue("Fee", infoHolder.fee)
|
||||
appendKeyValue("Phone model", infoHolder.phoneModel)
|
||||
appendKeyValue("OS version", infoHolder.osVersion)
|
||||
appendKeyValue("App version", infoHolder.appVersion)
|
||||
appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
|
||||
// appendKeyValue("Transaction HEX", infoHolder.transactionHex)
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
|
||||
class FeedbackEmail : EmailData {
|
||||
override val subject: String = "Tangem Tap feedback"
|
||||
override val mainMessage: String = "Hi Tangem,"
|
||||
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
|
||||
val builder = StringBuilder()
|
||||
builder.appendKeyValue("Card ID", infoHolder.cardId)
|
||||
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
|
||||
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
|
||||
|
||||
infoHolder.walletsInfo.forEach {
|
||||
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
|
||||
builder.appendKeyValue("Wallet address", it.address)
|
||||
builder.appendKeyValue("Explorer link", it.explorerLink)
|
||||
}
|
||||
// appendKeyValue("Outputs count", infoHolder.outputsCount)
|
||||
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
|
||||
builder.appendKeyValue("OS version", infoHolder.osVersion)
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder {
|
||||
return this.append("$key: $value\n")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_binance.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_btc.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_btc_cash.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_cardano.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_eth.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_ltc.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_rsk.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |