diff --git a/.gitignore b/.gitignore index 6b7cc463063..ef378ed8f74 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ gen/ .gradle/ /build app/release +app/debug # Fastlane fastlane/report.xml @@ -42,4 +43,5 @@ proguard/ .navigation/ ### Android Patch ### -gen-external-apklibs \ No newline at end of file +gen-external-apklibs +/app/google-services.json diff --git a/README.md b/README.md index 67c65fc1ec0..e168208b179 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# ASF Wallet for Android +# AppCoins Wallet for Android [![Build Status](https://travis-ci.org/TrustWallet/trust-wallet-android.svg?branch=master)](https://travis-ci.org/TrustWallet/trust-wallet-android) [![License](https://img.shields.io/badge/license-GPL3-green.svg?style=flat)](https://github.com/fastlane/fastlane/blob/master/LICENSE) -Welcome to ASF Wallet's open source Android app! +Welcome to AppCoins Wallet's open source Android app! ## Getting Started @@ -14,7 +14,7 @@ Welcome to ASF Wallet's open source Android app! ## Contributing As an open source project, it is our intention that this project may receive -contributions from the community. Ultimately, our goal in the ASF is to build +contributions from the community. Ultimately, our goal in the AppCoins is to build technology that is useful to our users. In order to submit feedback and report bugs, we consider the best way is to open @@ -24,4 +24,4 @@ and steps to reproduce the reported bugs. ## Credit -We forked the [Trust Wallet](https://trustwalletapp.com) for Android project to bootstrap the ASF wallet development. We considered it the best alternative regarding already implemented functionalities and forked it with the intention to contribute back with implementations and work that would be considered relevant. +We forked the [Trust Wallet](https://trustwalletapp.com) for Android project to bootstrap the ASF wallet development. We considered it the best alternative regarding already implemented functionalities and forked it with the intention to contribute back with implementations and work that would be considered relevant. diff --git a/airdrop/build.gradle b/airdrop/build.gradle index a6e1de3fd5a..a74efdb6198 100644 --- a/airdrop/build.gradle +++ b/airdrop/build.gradle @@ -1,17 +1,12 @@ apply plugin: 'java-library' - -repositories { - maven { url 'https://maven.fabric.io/public' } -} - dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "com.squareup.retrofit2:adapter-rxjava2:$project.retrofitVersion" - implementation "com.google.code.gson:gson:$project.gsonVersion" - implementation "org.jetbrains:annotations:$project.jetbrainsAnnotationsVersion" - testImplementation "junit:junit:$project.junitVersion" - testImplementation "org.mockito:mockito-core:$project.mockitoCoreVersion" + implementation "com.squareup.retrofit2:adapter-rxjava2:$project.retrofit_version" + implementation "com.google.code.gson:gson:$project.gson_version" + implementation "org.jetbrains:annotations:$project.jetbrains_annotations_version" + testImplementation "junit:junit:$project.junit_version" + testImplementation "org.mockito:mockito-core:$project.mockito_version" } sourceCompatibility = "1.8" diff --git a/airdrop/src/main/java/com/asfoundation/wallet/Airdrop.java b/airdrop/src/main/java/com/asfoundation/wallet/Airdrop.java index ce290d8c0cf..46e11975d1e 100644 --- a/airdrop/src/main/java/com/asfoundation/wallet/Airdrop.java +++ b/airdrop/src/main/java/com/asfoundation/wallet/Airdrop.java @@ -29,7 +29,7 @@ public void request(String walletAddress, int chainId, String captchaAnswer) { .flatMapCompletable(airDropResponse -> { switch (airDropResponse.getStatus()) { case WRONG_CAPTCHA: - return Completable.fromAction(() -> publishCaptchaError()); + return Completable.fromAction(this::publishCaptchaError); default: case OK: return waitForTransactions(airDropResponse); @@ -50,11 +50,10 @@ public void stop() { private Completable waitForTransactions(AirdropService.AirDropResponse airDropResponse) { return Single.fromCallable(() -> { List list = new ArrayList<>(); + list.add(transactionService.waitForTransactionToComplete( + airDropResponse.getAppcoinsTransaction())); list.add( - transactionService.waitForTransactionToComplete(airDropResponse.getAppcoinsTransaction(), - airDropResponse.getChainId())); - list.add(transactionService.waitForTransactionToComplete(airDropResponse.getEthTransaction(), - airDropResponse.getChainId())); + transactionService.waitForTransactionToComplete(airDropResponse.getEthTransaction())); return list; }) .flatMapCompletable(list -> Completable.merge(list) diff --git a/airdrop/src/main/java/com/asfoundation/wallet/AirdropService.java b/airdrop/src/main/java/com/asfoundation/wallet/AirdropService.java index 4cae1b07c7d..d26deeefdf5 100644 --- a/airdrop/src/main/java/com/asfoundation/wallet/AirdropService.java +++ b/airdrop/src/main/java/com/asfoundation/wallet/AirdropService.java @@ -25,7 +25,7 @@ public AirdropService(Api api, Gson gson, Scheduler scheduler) { this.scheduler = scheduler; } - public Single requestAirdrop(String walletAddress, Integer chainId, + Single requestAirdrop(String walletAddress, Integer chainId, String captchaAnswer) { return api.requestCoins(walletAddress, chainId, captchaAnswer) .subscribeOn(scheduler) @@ -46,7 +46,7 @@ public Single requestAirdrop(String walletAddress, Integer chai }); } - public Single requestCaptcha(String walletAddress) { + Single requestCaptcha(String walletAddress) { return api.requestCaptcha(walletAddress) .map(CaptchaResponse::getUrl); } @@ -72,12 +72,8 @@ public static class AirDropResponse { @SerializedName("chain_id") private int chainId; @SerializedName("description") private String description; - private AirDropResponse() { - status = Status.OK; - } - - public AirDropResponse(Status status, String appcoinsTransaction, String ethTransaction, - int chainId, String description) { + AirDropResponse(Status status, String appcoinsTransaction, String ethTransaction, int chainId, + String description) { this.status = status; this.appcoinsTransaction = appcoinsTransaction; this.ethTransaction = ethTransaction; diff --git a/airdrop/src/main/java/com/asfoundation/wallet/TransactionService.java b/airdrop/src/main/java/com/asfoundation/wallet/TransactionService.java index c0badce56f5..41c9f46b223 100644 --- a/airdrop/src/main/java/com/asfoundation/wallet/TransactionService.java +++ b/airdrop/src/main/java/com/asfoundation/wallet/TransactionService.java @@ -3,5 +3,5 @@ import io.reactivex.Completable; public interface TransactionService { - Completable waitForTransactionToComplete(String transactionHash, int chainId); + Completable waitForTransactionToComplete(String transactionHash); } diff --git a/airdrop/src/test/java/com/asfoundation/wallet/AirdropTest.java b/airdrop/src/test/java/com/asfoundation/wallet/AirdropTest.java index c8c8c710194..7de70e092c0 100644 --- a/airdrop/src/test/java/com/asfoundation/wallet/AirdropTest.java +++ b/airdrop/src/test/java/com/asfoundation/wallet/AirdropTest.java @@ -10,7 +10,6 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; @@ -29,7 +28,7 @@ @Before public void setUp() { when(airdropService.requestCaptcha(WALLET_ADDRESS)).thenReturn(Single.just(CAPTCHA)); - when(transactionService.waitForTransactionToComplete(anyString(), anyInt())).thenReturn( + when(transactionService.waitForTransactionToComplete(anyString())).thenReturn( Completable.complete()); airdrop = new Airdrop(transactionService, BehaviorSubject.create(), airdropService); } diff --git a/app/build.gradle b/app/build.gradle index 0c14cf55edd..961bcd9757d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,44 +1,27 @@ import groovy.json.JsonSlurper -buildscript { - repositories { - maven { url 'https://maven.fabric.io/public' } - } - - dependencies { - classpath 'io.fabric.tools:gradle:1.25.2' - } -} apply plugin: 'com.android.application' -apply plugin: 'io.fabric' -apply plugin: 'realm-android' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' - -repositories { - maven { url 'https://maven.fabric.io/public' } -} - +apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.gms.google-services' android { - compileSdkVersion 27 - buildToolsVersion '27.0.3' + buildToolsVersion '29.0.3' + compileSdkVersion 29 defaultConfig { def inputFile = new File("appcoins-services.json") def json = new JsonSlurper().parseText(inputFile.text) - buildConfigField 'String', 'DEFAULT_OEM_ADREESS', - "\"" + json.oems.default_address + "\"" - buildConfigField 'String', 'DEFAULT_STORE_ADREESS', - "\"" + json.stores.default_address + "\"" + buildConfigField 'String', 'DEFAULT_OEM_ADDRESS', "\"" + json.oems.default_address + "\"" + buildConfigField 'String', 'DEFAULT_STORE_ADDRESS', "\"" + json.stores.default_address + "\"" - applicationId "com.asfoundation.wallet" + applicationId "com.appcoins.wallet" minSdkVersion 21 - targetSdkVersion 27 - versionCode 10 - versionName "0.3.3a" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - vectorDrawables.useSupportLibrary = true + targetSdkVersion 29 + versionCode 173 + versionName "1.16.3.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true //room schemas location @@ -49,57 +32,88 @@ android { } buildConfigField 'int', 'DB_VERSION', '5' - - buildConfigField 'String', 'ROPSTEN_DEFAULT_TOKEN_SYMBOL', - "\"" + project.ROPSTEN_DEFAULT_TOKEN_SYMBOL + "\"" + buildConfigField 'int', 'BILLING_SUPPORTED_VERSION', project.BILLING_SUPPORTED_VERSION + buildConfigField 'String', 'ROPSTEN_DEFAULT_TOKEN_SYMBOL', project.ROPSTEN_DEFAULT_TOKEN_SYMBOL buildConfigField 'String', 'ROPSTEN_DEFAULT_TOKEN_ADDRESS', - "\"" + project.ROPSTEN_DEFAULT_TOKEN_ADDRESS + "\"" + project.ROPSTEN_DEFAULT_TOKEN_ADDRESS buildConfigField 'String', 'MAIN_NETWORK_DEFAULT_TOKEN_NAME', - "\"" + project.MAIN_NETWORK_DEFAULT_TOKEN_NAME + "\"" + project.MAIN_NETWORK_DEFAULT_TOKEN_NAME buildConfigField 'int', 'ROPSTEN_DEFAULT_TOKEN_DECIMALS', project.ROPSTEN_DEFAULT_TOKEN_DECIMALS - manifestPlaceholders.fabricApiKey = "${project.ASF_WALLET_FABRIC_KEY}" - buildConfigField 'String', 'MAIN_NETWORK_DEFAULT_TOKEN_SYMBOL', - "\"" + project.MAIN_NETWORK_DEFAULT_TOKEN_SYMBOL + "\"" + project.MAIN_NETWORK_DEFAULT_TOKEN_SYMBOL buildConfigField 'String', 'MAIN_NETWORK_DEFAULT_TOKEN_ADDRESS', - "\"" + project.MAIN_NETWORK_DEFAULT_TOKEN_ADDRESS + "\"" - buildConfigField 'String', 'ROPSTEN_DEFAULT_TOKEN_NAME', - "\"" + project.ROPSTEN_DEFAULT_TOKEN_NAME + "\"" + project.MAIN_NETWORK_DEFAULT_TOKEN_ADDRESS + buildConfigField 'String', 'ROPSTEN_DEFAULT_TOKEN_NAME', project.ROPSTEN_DEFAULT_TOKEN_NAME buildConfigField 'int', 'MAIN_NETWORK_DEFAULT_TOKEN_DECIMALS', project.MAIN_NETWORK_DEFAULT_TOKEN_DECIMALS - buildConfigField 'String', 'MAIN_NETWORK_ASF_ADS_CONTRACT_ADDRESS', - "\"" + project.MAIN_NETWORK_ASF_ADS_CONTRACT_ADDRESS + "\"" - buildConfigField 'String', 'ROPSTEN_NETWORK_ASF_ADS_CONTRACT_ADDRESS', - "\"" + project.ROPSTEN_NETWORK_ASF_ADS_CONTRACT_ADDRESS + "\"" - buildConfigField 'String', 'REGISTER_PROOF_GAS_LIMIT', - "\"" + project.REGISTER_PROOF_GAS_LIMIT + "\"" - buildConfigField 'String', 'PAYMENT_GAS_LIMIT', - "\"" + project.PAYMENT_GAS_LIMIT + "\"" + buildConfigField 'String', 'REGISTER_PROOF_GAS_LIMIT', project.REGISTER_PROOF_GAS_LIMIT + buildConfigField 'String', 'PAYMENT_GAS_LIMIT', project.PAYMENT_GAS_LIMIT + buildConfigField 'String', 'FLURRY_APK_KEY', project.FLURRY_APK_KEY + buildConfigField 'String', 'PAYMENT_HOST_ROPSTEN_NETWORK', project.PAYMENT_HOST_DEV + buildConfigField 'String', 'SECOND_PAYMENT_HOST', project.SECOND_PAYMENT_HOST + buildConfigField 'String', 'TRANSACTION_DETAILS_HOST', project.TRANSACTION_DETAILS_HOST + buildConfigField 'String', 'TRANSACTION_DETAILS_HOST_ROPSTEN', + project.TRANSACTION_DETAILS_HOST_ROPSTEN + buildConfigField 'String', 'RAKAM_BASE_HOST', project.RAKAM_BASE_HOST + buildConfigField 'String', 'RAKAM_API_KEY', project.RAKAM_API_KEY + resValue "string", "facebook_app_id", project.FACEBOOK_APP_KEY + buildConfigField 'String', 'INFURA_API_KEY_MAIN', project.INFURA_API_KEY_MAIN + buildConfigField 'String', 'INFURA_API_KEY_ROPSTEN', project.INFURA_API_KEY_ROPSTEN + buildConfigField 'String', 'AMPLITUDE_API_KEY', project.AMPLITUDE_API_KEY + manifestPlaceholders.facebookKey = "${project.FACEBOOK_APP_KEY}" } signingConfigs { release { - storeFile = file(project.ASF_WALLET_STORE_FILE) - storePassword = project.ASF_WALLET_STORE_PASSWORD - keyAlias = project.ASF_WALLET_KEY_ALIAS - keyPassword = project.ASF_WALLET_KEY_PASSWORD + storeFile = file(project.BDS_WALLET_STORE_FILE) + storePassword = project.BDS_WALLET_STORE_PASSWORD + keyAlias = project.BDS_WALLET_KEY_ALIAS + keyPassword = project.BDS_WALLET_KEY_PASSWORD } } buildTypes { release { - buildConfigField 'int', 'LEADING_ZEROS_ON_PROOF_OF_ATTENTION', - project.LEADING_ZEROS_ON_PROOF_OF_ATTENTION_RELEASE minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release + buildConfigField 'int', 'LEADING_ZEROS_ON_PROOF_OF_ATTENTION', + project.LEADING_ZEROS_ON_PROOF_OF_ATTENTION_RELEASE + buildConfigField 'String', 'BASE_HOST', project.BASE_HOST_PROD + buildConfigField 'String', 'BACKEND_HOST', project.BACKEND_HOST_PROD + buildConfigField 'String', 'BDS_BASE_HOST', project.BDS_BASE_HOST_PROD + buildConfigField 'String', 'MY_APPCOINS_BASE_HOST', project.MY_APPCOINS_BASE_HOST + buildConfigField 'String', 'PAYMENT_HOST', project.PAYMENT_HOST + buildConfigField 'String', 'CATAPPULT_BASE_HOST', project.CATAPPULT_BASE_HOST_PROD + buildConfigField 'String', 'APTOIDE_PKG_NAME', project.APTOIDE_PACKAGE_NAME + buildConfigField 'String', 'INTERCOM_API_KEY', project.INTERCOM_API_KEY + buildConfigField 'String', 'INTERCOM_APP_ID', project.INTERCOM_APP_ID + buildConfigField 'String', 'ADYEN_PUBLIC_KEY', project.ADYEN_PUBLIC_KEY + buildConfigField 'String', 'SENTRY_DSN_KEY', project.SENTRY_DSN_KEY + manifestPlaceholders.paymentHost = "${project.MANIFEST_PAYMENT_HOST}" + manifestPlaceholders.secondPaymentHost = "${project.MANIFEST_SECOND_PAYMENT_HOST}" } debug { minifyEnabled false - buildConfigField 'int', 'LEADING_ZEROS_ON_PROOF_OF_ATTENTION', - project.LEADING_ZEROS_ON_PROOF_OF_ATTENTION_DEBUG proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' applicationIdSuffix ".dev" + versionNameSuffix ".dev" + buildConfigField 'int', 'LEADING_ZEROS_ON_PROOF_OF_ATTENTION', + project.LEADING_ZEROS_ON_PROOF_OF_ATTENTION_DEBUG + buildConfigField 'String', 'BASE_HOST', project.BASE_HOST_DEV + buildConfigField 'String', 'BACKEND_HOST', project.BACKEND_HOST_DEV + buildConfigField 'String', 'BDS_BASE_HOST', project.BDS_BASE_HOST_DEV + buildConfigField 'String', 'MY_APPCOINS_BASE_HOST', project.MY_APPCOINS_BASE_HOST_DEV + buildConfigField 'String', 'CATAPPULT_BASE_HOST', project.CATAPPULT_BASE_HOST_DEV + buildConfigField 'String', 'APTOIDE_PKG_NAME', project.APTOIDE_PACKAGE_NAME_DEV + buildConfigField 'String', 'PAYMENT_HOST', project.PAYMENT_HOST_DEV + buildConfigField 'String', 'SECOND_PAYMENT_HOST', project.PAYMENT_HOST_DEV + buildConfigField 'String', 'INTERCOM_API_KEY', project.INTERCOM_API_KEY_DEV + buildConfigField 'String', 'INTERCOM_APP_ID', project.INTERCOM_APP_ID_DEV + buildConfigField 'String', 'ADYEN_PUBLIC_KEY', project.ADYEN_PUBLIC_KEY_DEV + buildConfigField 'String', 'SENTRY_DSN_KEY', project.SENTRY_DSN_KEY_DEV + manifestPlaceholders.paymentHost = "${project.MANIFEST_PAYMENT_HOST_DEV}" + manifestPlaceholders.secondPaymentHost = "${project.MANIFEST_PAYMENT_HOST_DEV}" applicationVariants.all { variant -> renameArtifact(defaultConfig) } } @@ -124,95 +138,116 @@ android { } } - dependencies { - // Etherium client - implementation "org.web3j:core:$project.web3jVersion" - implementation "org.ethereum:geth:$project.gethVersion" - // Http client - implementation "com.squareup.retrofit2:retrofit:$project.retrofitVersion" - implementation "com.squareup.retrofit2:converter-gson:$project.retrofitVersion" - implementation "com.squareup.retrofit2:adapter-rxjava2:$project.retrofitVersion" - implementation "com.squareup.okhttp3:okhttp:$project.okhttpVersion" - implementation "com.google.code.gson:gson:$project.gsonVersion" - implementation "com.squareup.picasso:picasso:$project.picassoVersion" - - implementation "android.arch.lifecycle:runtime:1.1.1" - implementation "android.arch.lifecycle:extensions:1.1.1" - - implementation "com.android.support:appcompat-v7:$project.supportVersion" - implementation "com.android.support:design:$project.supportVersion" - implementation "com.android.support:support-vector-drawable:$project.supportVersion" - implementation "com.android.support:recyclerview-v7:$project.supportVersion" - implementation "com.android.support:cardview-v7:$project.supportVersion" - - implementation "com.android.support:multidex:1.0.3" - // Bar code scanning - implementation "com.google.zxing:core:3.3.1" - implementation "com.google.android.gms:play-services-vision:12.0.0" - // Sugar - implementation "com.android.support.constraint:constraint-layout:1.0.2" - - implementation "com.github.apl-devs:appintro:v4.2.2" - implementation 'com.romandanylyk:pageindicatorview:1.0.0' - implementation "com.journeyapps:zxing-android-embedded:3.2.0@aar" - // ReactiveX - implementation "io.reactivex.rxjava2:rxjava:$project.rxJavaVersion" - implementation "io.reactivex.rxjava2:rxandroid:$project.rxAndroidVersion" - // Dagger 2 - // Dagger core - implementation "com.google.dagger:dagger:$project.daggerVersion" - annotationProcessor "com.google.dagger:dagger-compiler:$project.daggerVersion" - // Dagger Android - implementation "com.google.dagger:dagger-android-support:$project.daggerVersion" - annotationProcessor "com.google.dagger:dagger-android-processor:$project.daggerVersion" - // if you are not using support library, include this instead - implementation "com.google.dagger:dagger-android:$project.daggerVersion" - - implementation "com.github.walleth.kethereum:erc681:$project.erc681Version" - // Tests - testImplementation "junit:junit:$junitVersion" - testImplementation "org.mockito:mockito-core:$project.mockitoCoreVersion" - androidTestImplementation("com.android.support.test.espresso:espresso-core:2.2.2", { - exclude group: "com.android.support", module: "support-annotations" - }) - androidTestImplementation('tools.fastlane:screengrab:1.1.0', { - exclude group: 'com.android.support', module: 'support-annotations' - }) - // Fabric - implementation('com.crashlytics.sdk.android:crashlytics:2.8.0@aar') { - transitive = true - } - implementation('com.crashlytics.sdk.android:answers:1.4.1@aar') { - transitive = true - } - // PW implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(":tn") - implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' + implementation project(path: ':airdrop') + implementation project(path: ':billing') + implementation project(path: ':commons') + implementation project(path: ':gamification') + implementation project(path: ':permissions') + implementation project(path: ':appcoinsRewards') + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "android.arch.persistence.room:runtime:$project.roomVersion" - annotationProcessor "android.arch.persistence.room:compiler:$project.roomVersion" - // RxJava support for Room - implementation "android.arch.persistence.room:rxjava2:$project.roomVersion" - // Pincode Lollipin - // compile('com.github.omadahealth:lollipin:2.1.0@aar') { - // transitive = true - // } - implementation project(path: ':airdrop') + implementation "androidx.appcompat:appcompat:$project.appcompat_version" + implementation "androidx.vectordrawable:vectordrawable:$project.vector_drawable_version" + implementation "androidx.recyclerview:recyclerview:$project.recyclerview_version" + implementation "androidx.cardview:cardview:$project.cardview_version" + implementation "androidx.constraintlayout:constraintlayout:$project.constraintlayout_version" + implementation "androidx.palette:palette:$project.palette_version" + implementation "androidx.preference:preference:$project.preference_version" + implementation "androidx.multidex:multidex:$project.multidex_version" + implementation "androidx.viewpager2:viewpager2:$project.viewpager_version" + implementation "androidx.room:room-runtime:$project.room_version" + implementation "androidx.room:room-rxjava2:$project.room_version" + kapt "androidx.room:room-compiler:$project.room_version" + + implementation "android.arch.lifecycle:runtime:$project.lifecycle_version" + implementation "android.arch.lifecycle:extensions:$project.lifecycle_version" + + implementation "com.squareup.retrofit2:retrofit:$project.retrofit_version" + implementation "com.squareup.retrofit2:converter-gson:$project.retrofit_version" + implementation "com.squareup.retrofit2:adapter-rxjava2:$project.retrofit_version" + implementation "com.squareup.okhttp3:okhttp:$project.okhttp_version" + implementation "com.google.code.gson:gson:$project.gson_version" + + implementation "com.google.android.material:material:$project.material_version" + implementation "com.google.android.gms:play-services-vision:$project.play_services_vision_version" + implementation "com.google.zxing:core:$project.zxing_version" + implementation "com.journeyapps:zxing-android-embedded:$project.zxing_android_version" + + implementation "com.google.dagger:dagger:$project.dagger_version" + kapt "com.google.dagger:dagger-android-processor:$project.dagger_version" + kapt "com.google.dagger:dagger-compiler:$project.dagger_version" + implementation "com.google.dagger:dagger-android-support:$project.dagger_version" + + implementation "io.reactivex.rxjava2:rxjava:$project.rxjava_version" + implementation "io.reactivex.rxjava2:rxandroid:$project.rxandroid_version" + implementation "com.jakewharton.rxbinding2:rxbinding:$project.rxbinding_version" + // Rx Lifecycle + implementation "com.trello:rxlifecycle:$project.rxlifecycle_version" + // If you want pre-written Activities and Fragments you can subclass as providers + implementation "com.trello:rxlifecycle-components:$project.rxlifecycle_version" + + implementation "com.adyen.checkout:card-ui:$project.adyen_version" + implementation "com.adyen.checkout:redirect:$project.adyen_version" + implementation "com.adyen.checkout:3ds2:$project.adyen_version" + + implementation "io.intercom.android:intercom-sdk:$project.intercom_version" + + implementation "io.rakam:android-sdk:$project.rakam_version" + + implementation "com.github.bumptech.glide:glide:$project.glide_version" + kapt "com.github.bumptech.glide:compiler:$project.glide_version" + + implementation "com.flurry.android:analytics:$project.flurry_version" + + implementation "com.facebook.android:facebook-android-sdk:$project.facebook_sdk_version" + implementation "aptoide-client-v8:aptoide-analytics-core:$project.aptoide_analytics_version" + + implementation "com.asfoundation:applications:$project.asf_applications_version" + implementation "com.asfoundation:appcoins-contract-proxy:$project.asf_sdk_version" + implementation "com.asfoundation:ethereumj-android:$project.ethereumj_sdk_version" + + implementation "com.google.firebase:firebase-messaging:$project.firebase_messaging" + + implementation "com.romandanylyk:pageindicatorview:$project.pageindicatorview_version" + + implementation "org.web3j:core:$project.web3j_version" + + implementation "com.github.walleth.kethereum:erc681:$project.erc681_version" + + implementation "com.airbnb.android:lottie:$project.lottie_version" + + implementation "com.hbb20:ccp:$project.cpp_version" + + implementation "com.hendraanggrian.material:collapsingtoolbarlayout-subtitle:$project.collapsingtoolbarlayout_version" + + implementation "io.sentry:sentry-android:$project.sentry_version" + + implementation "com.facebook.shimmer:shimmer:$project.shimmer_version" + + //Amplitude + implementation "com.amplitude:android-sdk:$project.amplitude_version" + + implementation "androidx.biometric:biometric:$project.biometrics_version" + + testImplementation "junit:junit:$project.junit_version" + testImplementation "org.mockito:mockito-core:$project.mockito_version" + androidTestImplementation "androidx.test.ext:junit:$project.test_ext_version" } def renameArtifact(defaultConfig) { android.applicationVariants.all { variant -> variant.outputs.all { - def formattedDate = new Date().format('yyMMdd-HHmm') - def fileName = "ASF_Wallet_V${defaultConfig.versionCode}_${formattedDate}_${variant.name}" - outputFileName = new File("${fileName}.apk") + def SEP = "_" + def buildType = variant.variantData.variantConfiguration.buildType.name + def versionName = variant.versionName + def versionCode = defaultConfig.versionCode + def fileName = + "AppCoins_Wallet_v" + versionName + SEP + versionCode + SEP + buildType + ".apk" + outputFileName = new File("${fileName}") } } -} - -// execute android tests before realising a new apk -tasks.whenTaskAdded { task -> if (task.name == 'assembleRelease') task.dependsOn('test') } \ No newline at end of file diff --git a/app/libs/extractor.jar b/app/libs/extractor.jar new file mode 100644 index 00000000000..701a08e69fd Binary files /dev/null and b/app/libs/extractor.jar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index da7ddd2d412..a2ed00edaea 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -23,3 +23,5 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + -keep class com.google.android.gms.ads.** { *; } + -dontwarn okio.** \ No newline at end of file diff --git a/app/schemas/com.asfoundation.wallet.permissions.repository.PermissionsDatabase/1.json b/app/schemas/com.asfoundation.wallet.permissions.repository.PermissionsDatabase/1.json new file mode 100644 index 00000000000..8b5a60e0708 --- /dev/null +++ b/app/schemas/com.asfoundation.wallet.permissions.repository.PermissionsDatabase/1.json @@ -0,0 +1,57 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "a1f470784bdebefa217d471f018cecf6", + "entities": [ + { + "tableName": "PermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `wallet_address` TEXT NOT NULL, `package_name` TEXT NOT NULL, `apk_signature` TEXT NOT NULL, `permissions` TEXT NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletAddress", + "columnName": "wallet_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "apkSignature", + "columnName": "apk_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"a1f470784bdebefa217d471f018cecf6\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.asfoundation.wallet.promotions.PromotionDatabase/1.json b/app/schemas/com.asfoundation.wallet.promotions.PromotionDatabase/1.json new file mode 100644 index 00000000000..2bfeac3576c --- /dev/null +++ b/app/schemas/com.asfoundation.wallet.promotions.PromotionDatabase/1.json @@ -0,0 +1,208 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "fc8b2b301355431b2c526dae637aee69", + "entities": [ + { + "tableName": "PromotionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `priority` INTEGER, `bonus` REAL NOT NULL, `total_spend` TEXT NOT NULL, `total_earned` TEXT NOT NULL, `level` INTEGER NOT NULL, `next_level_amount` TEXT, `status` TEXT NOT NULL, `max_amount` TEXT NOT NULL, `available` INTEGER NOT NULL, `bundle` INTEGER NOT NULL, `completed` INTEGER NOT NULL, `currency` TEXT NOT NULL, `symbol` TEXT NOT NULL, `invited` INTEGER NOT NULL, `link` TEXT, `pending_amount` TEXT NOT NULL, `received_amount` TEXT NOT NULL, `user_status` TEXT, `min_amount` TEXT NOT NULL, `amount` TEXT NOT NULL, `current_progress` REAL, `description` TEXT NOT NULL, `end_date` INTEGER NOT NULL, `icon` TEXT, `linked_promotion_id` TEXT, `objective_progress` REAL, `start_date` INTEGER, `title` TEXT NOT NULL, `view_type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bonus", + "columnName": "bonus", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "totalSpend", + "columnName": "total_spend", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "totalEarned", + "columnName": "total_earned", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nextLevelAmount", + "columnName": "next_level_amount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maxAmount", + "columnName": "max_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "available", + "columnName": "available", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bundle", + "columnName": "bundle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "invited", + "columnName": "invited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pendingAmount", + "columnName": "pending_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "receivedAmount", + "columnName": "received_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "minAmount", + "columnName": "min_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentProgress", + "columnName": "current_progress", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endDate", + "columnName": "end_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "linkedPromotionId", + "columnName": "linked_promotion_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "objectiveProgress", + "columnName": "objective_progress", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "start_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewType", + "columnName": "view_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fc8b2b301355431b2c526dae637aee69')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.asfoundation.wallet.repository.TransactionsDatabase/1.json b/app/schemas/com.asfoundation.wallet.repository.TransactionsDatabase/1.json new file mode 100644 index 00000000000..11c71c32364 --- /dev/null +++ b/app/schemas/com.asfoundation.wallet.repository.TransactionsDatabase/1.json @@ -0,0 +1,188 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "46a7d72a6f5d72c0b136298cc30a5c4c", + "entities": [ + { + "tableName": "TransactionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`transactionId` TEXT NOT NULL, `relatedWallet` TEXT NOT NULL, `approveTransactionId` TEXT, `type` TEXT NOT NULL, `timeStamp` INTEGER NOT NULL, `processedTime` INTEGER NOT NULL, `status` TEXT NOT NULL, `value` TEXT NOT NULL, `from` TEXT NOT NULL, `to` TEXT NOT NULL, `currency` TEXT, `operations` TEXT, `sourceName` TEXT, `description` TEXT, `iconType` TEXT, `uri` TEXT, PRIMARY KEY(`transactionId`))", + "fields": [ + { + "fieldPath": "transactionId", + "columnName": "transactionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relatedWallet", + "columnName": "relatedWallet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "approveTransactionId", + "columnName": "approveTransactionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeStamp", + "columnName": "timeStamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "processedTime", + "columnName": "processedTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "from", + "columnName": "from", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "operations", + "columnName": "operations", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.icon.iconType", + "columnName": "iconType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.icon.uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "transactionId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TransactionDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceName` TEXT, `description` TEXT, `iconType` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`iconType`, `uri`))", + "fields": [ + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "icon.iconType", + "columnName": "iconType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon.uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "iconType", + "uri" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icon", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`iconType` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "iconType", + "columnName": "iconType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uri" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"46a7d72a6f5d72c0b136298cc30a5c4c\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.asfoundation.wallet.repository.TransactionsDatabase/2.json b/app/schemas/com.asfoundation.wallet.repository.TransactionsDatabase/2.json new file mode 100644 index 00000000000..e67736ea8ec --- /dev/null +++ b/app/schemas/com.asfoundation.wallet.repository.TransactionsDatabase/2.json @@ -0,0 +1,190 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "ffd79d5a0b844f4e75773fba329c3eb0", + "entities": [ + { + "tableName": "TransactionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`transactionId` TEXT NOT NULL, `relatedWallet` TEXT NOT NULL, `approveTransactionId` TEXT, `type` TEXT NOT NULL, `timeStamp` INTEGER NOT NULL, `processedTime` INTEGER NOT NULL, `status` TEXT NOT NULL, `value` TEXT NOT NULL, `from` TEXT NOT NULL, `to` TEXT NOT NULL, `currency` TEXT, `operations` TEXT, `sourceName` TEXT, `description` TEXT, `iconType` TEXT, `uri` TEXT, PRIMARY KEY(`transactionId`, `relatedWallet`))", + "fields": [ + { + "fieldPath": "transactionId", + "columnName": "transactionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relatedWallet", + "columnName": "relatedWallet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "approveTransactionId", + "columnName": "approveTransactionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeStamp", + "columnName": "timeStamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "processedTime", + "columnName": "processedTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "from", + "columnName": "from", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "operations", + "columnName": "operations", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.icon.iconType", + "columnName": "iconType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.icon.uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "transactionId", + "relatedWallet" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TransactionDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceName` TEXT, `description` TEXT, `iconType` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`iconType`, `uri`))", + "fields": [ + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "icon.iconType", + "columnName": "iconType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon.uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "iconType", + "uri" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icon", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`iconType` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "iconType", + "columnName": "iconType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uri" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ffd79d5a0b844f4e75773fba329c3eb0')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.asfoundation.wallet.repository.TransactionsDatabase/3.json b/app/schemas/com.asfoundation.wallet.repository.TransactionsDatabase/3.json new file mode 100644 index 00000000000..581667da80c --- /dev/null +++ b/app/schemas/com.asfoundation.wallet.repository.TransactionsDatabase/3.json @@ -0,0 +1,208 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "8aa4bbfccd2c40c1a01de03624447371", + "entities": [ + { + "tableName": "TransactionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`transactionId` TEXT NOT NULL, `relatedWallet` TEXT NOT NULL, `approveTransactionId` TEXT, `type` TEXT NOT NULL, `subType` TEXT, `title` TEXT, `cardDescription` TEXT, `timeStamp` INTEGER NOT NULL, `processedTime` INTEGER NOT NULL, `status` TEXT NOT NULL, `value` TEXT NOT NULL, `from` TEXT NOT NULL, `to` TEXT NOT NULL, `currency` TEXT, `operations` TEXT, `sourceName` TEXT, `description` TEXT, `iconType` TEXT, `uri` TEXT, PRIMARY KEY(`transactionId`, `relatedWallet`))", + "fields": [ + { + "fieldPath": "transactionId", + "columnName": "transactionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relatedWallet", + "columnName": "relatedWallet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "approveTransactionId", + "columnName": "approveTransactionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subType", + "columnName": "subType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cardDescription", + "columnName": "cardDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeStamp", + "columnName": "timeStamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "processedTime", + "columnName": "processedTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "from", + "columnName": "from", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "operations", + "columnName": "operations", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.icon.iconType", + "columnName": "iconType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.icon.uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "transactionId", + "relatedWallet" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TransactionDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceName` TEXT, `description` TEXT, `iconType` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`iconType`, `uri`))", + "fields": [ + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "icon.iconType", + "columnName": "iconType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon.uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "iconType", + "uri" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icon", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`iconType` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "iconType", + "columnName": "iconType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uri" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8aa4bbfccd2c40c1a01de03624447371')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.asfoundation.wallet.repository.TransactionsDatabase/4.json b/app/schemas/com.asfoundation.wallet.repository.TransactionsDatabase/4.json new file mode 100644 index 00000000000..5572d35ddde --- /dev/null +++ b/app/schemas/com.asfoundation.wallet.repository.TransactionsDatabase/4.json @@ -0,0 +1,214 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "9f29fec227086eb127928bad729f361a", + "entities": [ + { + "tableName": "TransactionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`transactionId` TEXT NOT NULL, `relatedWallet` TEXT NOT NULL, `approveTransactionId` TEXT, `perk` TEXT, `type` TEXT NOT NULL, `subType` TEXT, `title` TEXT, `cardDescription` TEXT, `timeStamp` INTEGER NOT NULL, `processedTime` INTEGER NOT NULL, `status` TEXT NOT NULL, `value` TEXT NOT NULL, `from` TEXT NOT NULL, `to` TEXT NOT NULL, `currency` TEXT, `operations` TEXT, `sourceName` TEXT, `description` TEXT, `iconType` TEXT, `uri` TEXT, PRIMARY KEY(`transactionId`, `relatedWallet`))", + "fields": [ + { + "fieldPath": "transactionId", + "columnName": "transactionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relatedWallet", + "columnName": "relatedWallet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "approveTransactionId", + "columnName": "approveTransactionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "perk", + "columnName": "perk", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subType", + "columnName": "subType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cardDescription", + "columnName": "cardDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeStamp", + "columnName": "timeStamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "processedTime", + "columnName": "processedTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "from", + "columnName": "from", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "operations", + "columnName": "operations", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.icon.iconType", + "columnName": "iconType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "details.icon.uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "transactionId", + "relatedWallet" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TransactionDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceName` TEXT, `description` TEXT, `iconType` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`iconType`, `uri`))", + "fields": [ + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "icon.iconType", + "columnName": "iconType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon.uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "iconType", + "uri" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icon", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`iconType` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "iconType", + "columnName": "iconType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uri" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9f29fec227086eb127928bad729f361a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.asfoundation.wallet.ui.balance.database.BalanceDetailsDatabase/1.json b/app/schemas/com.asfoundation.wallet.ui.balance.database.BalanceDetailsDatabase/1.json new file mode 100644 index 00000000000..8a65741b903 --- /dev/null +++ b/app/schemas/com.asfoundation.wallet.ui.balance.database.BalanceDetailsDatabase/1.json @@ -0,0 +1,82 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "66deecc91471f7500268a522760ab383", + "entities": [ + { + "tableName": "balance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wallet_address` TEXT NOT NULL, `fiat_currency` TEXT NOT NULL, `fiat_symbol` TEXT NOT NULL, `eth_token_amount` TEXT NOT NULL, `eth_token_conversion` TEXT NOT NULL, `appc_token_amount` TEXT NOT NULL, `appc_token_conversion` TEXT NOT NULL, `credits_token_amount` TEXT NOT NULL, `credits_token_conversion` TEXT NOT NULL, PRIMARY KEY(`wallet_address`))", + "fields": [ + { + "fieldPath": "wallet", + "columnName": "wallet_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fiatCurrency", + "columnName": "fiat_currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fiatSymbol", + "columnName": "fiat_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ethAmount", + "columnName": "eth_token_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ethConversion", + "columnName": "eth_token_conversion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appcAmount", + "columnName": "appc_token_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appcConversion", + "columnName": "appc_token_conversion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creditsAmount", + "columnName": "credits_token_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creditsConversion", + "columnName": "credits_token_conversion", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "wallet_address" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '66deecc91471f7500268a522760ab383')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/asf/wallet/EthereumNetworkRepositoryTest.java b/app/src/androidTest/java/com/asf/wallet/EthereumNetworkRepositoryTest.java deleted file mode 100644 index 7af1a7c06d9..00000000000 --- a/app/src/androidTest/java/com/asf/wallet/EthereumNetworkRepositoryTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.asf.wallet; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; -import com.asfoundation.wallet.repository.EthereumNetworkRepository; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; -import com.asfoundation.wallet.repository.PreferenceRepositoryType; -import com.asfoundation.wallet.repository.SharedPreferenceRepository; -import org.junit.Before; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) public class EthereumNetworkRepositoryTest { - - private EthereumNetworkRepositoryType networkRepository; - - @Before public void setUp() { - Context context = InstrumentationRegistry.getTargetContext(); - PreferenceRepositoryType preferenceRepositoryType = new SharedPreferenceRepository(context); - networkRepository = new EthereumNetworkRepository(preferenceRepositoryType); - } -} diff --git a/app/src/androidTest/java/com/asf/wallet/GetKeystoreWalletRepoTest.java b/app/src/androidTest/java/com/asf/wallet/GetKeystoreWalletRepoTest.java deleted file mode 100644 index 9db6af6c311..00000000000 --- a/app/src/androidTest/java/com/asf/wallet/GetKeystoreWalletRepoTest.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.asf.wallet; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; -import android.util.Log; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.service.AccountKeystoreService; -import com.asfoundation.wallet.service.GethKeystoreAccountService; -import io.reactivex.observers.TestObserver; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -@RunWith(AndroidJUnit4.class) public class GetKeystoreWalletRepoTest { - static final String STORE_1 = - "{\"address\":\"eb1a948c6cc57fedf9271626404fc04a74ddd1e6\",\"crypto\":{\"cipher\":\"aes-128-ctr\",\"ciphertext\":\"6f6ba0b047f191f01df175255d0ef1eaf687905b3c22f9975d4cdec76f266d1e\",\"cipherparams\":{\"iv\":\"289195567cced5b5e6c8b18158c5f2ec\"},\"kdf\":\"scrypt\",\"kdfparams\":{\"dklen\":32,\"n\":4096,\"p\":6,\"r\":8,\"salt\":\"243df82bdd2569ecf5da25fd9db21cf5857be99ed64c7e664432f5ebef626ebe\"},\"mac\":\"87313234721b61a2c58b0d89f44847ea01df52c96fd5e3c8855efa0ecfd7ee06\"},\"id\":\"3cb467fc-7f98-435f-98e3-7f660e0368cc\",\"version\":3}"; - static final String PASS_1 = "1234"; - static final String ADDRESS_1 = "0xeb1a948c6cc57fedf9271626404fc04a74ddd1e6"; - - private AccountKeystoreService accountKeystoreService; - - @Before public void setUp() { - Context context = InstrumentationRegistry.getTargetContext(); - accountKeystoreService = - new GethKeystoreAccountService(new File(context.getFilesDir(), "store")); - } - - // Single signTransaction( - // Wallet signer, - // String signerPassword, - // String toAddress, - // String wei, - // long nonce, - // long chainId); - @Test public void testCreateAccount() { - TestObserver subscriber = new TestObserver<>(); - accountKeystoreService.createAccount("1234") - .toObservable() - .subscribe(subscriber); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - subscriber.assertNoErrors(); - - assertEquals(subscriber.valueCount(), 1); - deleteAccountStore(subscriber.values() - .get(0).address, "1234"); - } - - @Test public void testImportStore() { - TestObserver subscriber = accountKeystoreService.importKeystore(STORE_1, PASS_1, PASS_1) - .toObservable() - .test(); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - subscriber.assertNoErrors(); - - subscriber.assertOf(accountTestObserver -> { - assertEquals(accountTestObserver.valueCount(), 1); - assertEquals(accountTestObserver.values() - .get(0).address, ADDRESS_1); - assertTrue(accountTestObserver.values() - .get(0) - .sameAddress(ADDRESS_1)); - }); - deleteAccountStore(ADDRESS_1, PASS_1); - } - - @Test public void testDeleteStore() { - importAccountStore(STORE_1, PASS_1); - TestObserver subscriber = accountKeystoreService.deleteAccount(ADDRESS_1, PASS_1) - .test(); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - TestObserver accountListSubscriber = accountList(); - accountListSubscriber.awaitTerminalEvent(); - accountListSubscriber.assertComplete(); - assertEquals(accountListSubscriber.valueCount(), 1); - assertEquals(accountListSubscriber.values() - .get(0).length, 0); - } - - @Test public void testFetchAccounts() { - List createdWallets = new ArrayList<>(); - for (int i = 0; i < 100; i++) { - createdWallets.add(createAccountStore()); - } - TestObserver subscriber = accountKeystoreService.fetchAccounts() - .test(); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - assertEquals(subscriber.valueCount(), 1); - assertEquals(subscriber.values() - .get(0).length, 100); - - Wallet[] wallets = subscriber.values() - .get(0); - - for (int i = 0; i < 100; i++) { - assertTrue(createdWallets.get(i) - .sameAddress(wallets[i].address)); - } - for (Wallet wallet : createdWallets) { - deleteAccountStore(wallet.address, PASS_1); - } - } - - @Test public void testExportAccountStore() { - importAccountStore(STORE_1, PASS_1); - TestObserver subscriber = - accountKeystoreService.exportAccount(new Wallet(ADDRESS_1), PASS_1, PASS_1) - .test(); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - assertEquals(subscriber.valueCount(), 1); - Log.d("EXPORT_ACC", "Val: " + subscriber.values() - .get(0)); - String val = subscriber.values() - .get(0); - try { - JSONObject json = new JSONObject(val); - assertTrue(("0x" + json.getString("address")).equalsIgnoreCase(ADDRESS_1)); - } catch (Exception ex) { - ex.printStackTrace(); - } - deleteAccountStore(ADDRESS_1, PASS_1); - } - - private TestObserver accountList() { - return accountKeystoreService.fetchAccounts() - .test(); - } - - private void importAccountStore(String store, String password) { - TestObserver subscriber = - accountKeystoreService.importKeystore(store, password, password) - .toObservable() - .test(); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - } - - private void deleteAccountStore(String address, String password) { - accountKeystoreService.deleteAccount(address, password) - .toObservable() - .test() - .awaitTerminalEvent(); - } - - private Wallet createAccountStore() { - TestObserver subscriber = new TestObserver<>(); - accountKeystoreService.createAccount("1234") - .toObservable() - .subscribe(subscriber); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - assertEquals(subscriber.valueCount(), 1); - return subscriber.values() - .get(0); - } -} diff --git a/app/src/androidTest/java/com/asf/wallet/ImportPrivateKey.java b/app/src/androidTest/java/com/asf/wallet/ImportPrivateKey.java deleted file mode 100644 index b416363d753..00000000000 --- a/app/src/androidTest/java/com/asf/wallet/ImportPrivateKey.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.asf.wallet; - -import android.support.test.runner.AndroidJUnit4; -import com.asfoundation.wallet.controller.EtherStoreUtils; -import com.fasterxml.jackson.core.JsonProcessingException; -import java.io.UnsupportedEncodingException; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.web3j.crypto.CipherException; -import org.web3j.crypto.WalletFile; - -/** - * Created by marat on 11/22/17. - */ - -@RunWith(AndroidJUnit4.class) public class ImportPrivateKey { - /* - Keystore - {"address":"7d788fc8df7165b11a19f201558fcc3590fd8d97","crypto":{"cipher":"aes-128-ctr","ciphertext":"716c1aeeb9237c925d9b4ec63360e5541030a64e2a1a8fb91a3fb703acb20cd8","cipherparams":{"iv":"0d19830ebc74ed223442daa5d0e67912"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"eb3017442d9edcfb2f05185603aa66fa635ad99284b7ac55764928db5de461ca"},"mac":"a0ad032bac0c2ad62d4eca2bf50f7814fbef91b78e5b29b025cc4f8a685902c5"},"id":"c0c9d734-147b-4df0-abe8-b5bcd54e6e96","version":3} - Private key - 68adf89afe85baa046919f904f7c1e3a9cb28ca8b3039c2bcb3fa5a980d3a165 - */ - - @Test public void privateKeyToKeystoreTest() - throws UnsupportedEncodingException, CipherException, JsonProcessingException { - String privateKey = "68adf89afe85baa046919f904f7c1e3a9cb28ca8b3039c2bcb3fa5a980d3a165"; - String passphrase = "x"; - WalletFile w = EtherStoreUtils.convertPrivateKeyToKeystoreFile(privateKey, passphrase); - - assert (w.getAddress() - .equals("7d788fc8df7165b11a19f201558fcc3590fd8d97")); - } -} diff --git a/app/src/androidTest/java/com/asf/wallet/PasswordManagerTest.java b/app/src/androidTest/java/com/asf/wallet/PasswordManagerTest.java deleted file mode 100644 index 1b4e3e58828..00000000000 --- a/app/src/androidTest/java/com/asf/wallet/PasswordManagerTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.asf.wallet; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import com.wallet.pwd.trustapp.PasswordManager; -import java.io.UnsupportedEncodingException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import org.junit.Test; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - -/** - * Created by marat on 11/14/17. - */ - -public class PasswordManagerTest { - @Test public void setGetPassword() - throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, - IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, - UnsupportedEncodingException, InvalidKeySpecException { - Context context = InstrumentationRegistry.getTargetContext(); - - PasswordManager.setPassword("myaddress", "mypassword", context); - assertThat(PasswordManager.getPassword("myaddress", context), is("mypassword")); - } - - @Test public void setGetPasswordLegacy() - throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, - IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, - UnsupportedEncodingException, InvalidKeySpecException { - Context context = InstrumentationRegistry.getTargetContext(); - - PasswordManager.setPasswordLegacy("myaddress", "mypassword", context); - assertThat(PasswordManager.getPassword("myaddress", context), is("mypassword")); - } -} diff --git a/app/src/androidTest/java/com/asf/wallet/WalletRepoTest.java b/app/src/androidTest/java/com/asf/wallet/WalletRepoTest.java deleted file mode 100644 index 0851bb3cb63..00000000000 --- a/app/src/androidTest/java/com/asf/wallet/WalletRepoTest.java +++ /dev/null @@ -1,211 +0,0 @@ -package com.asf.wallet; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; -import android.util.Log; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.EthereumNetworkRepository; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; -import com.asfoundation.wallet.repository.PreferenceRepositoryType; -import com.asfoundation.wallet.repository.SharedPreferenceRepository; -import com.asfoundation.wallet.repository.WalletRepository; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import com.asfoundation.wallet.service.AccountKeystoreService; -import com.asfoundation.wallet.service.GethKeystoreAccountService; -import io.reactivex.observers.TestObserver; -import java.io.File; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; -import org.json.JSONObject; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertTrue; - -@RunWith(AndroidJUnit4.class) public class WalletRepoTest { - - static final String STORE_1 = - "{\"address\":\"eb1a948c6cc57fedf9271626404fc04a74ddd1e6\",\"crypto\":{\"cipher\":\"aes-128-ctr\",\"ciphertext\":\"6f6ba0b047f191f01df175255d0ef1eaf687905b3c22f9975d4cdec76f266d1e\",\"cipherparams\":{\"iv\":\"289195567cced5b5e6c8b18158c5f2ec\"},\"kdf\":\"scrypt\",\"kdfparams\":{\"dklen\":32,\"n\":4096,\"p\":6,\"r\":8,\"salt\":\"243df82bdd2569ecf5da25fd9db21cf5857be99ed64c7e664432f5ebef626ebe\"},\"mac\":\"87313234721b61a2c58b0d89f44847ea01df52c96fd5e3c8855efa0ecfd7ee06\"},\"id\":\"3cb467fc-7f98-435f-98e3-7f660e0368cc\",\"version\":3}"; - static final String PASS_1 = "1234"; - static final String ADDRESS_1 = "0xeb1a948c6cc57fedf9271626404fc04a74ddd1e6"; - - protected WalletRepositoryType accountRepository; - - @Before public void setUp() { - Context context = InstrumentationRegistry.getTargetContext(); - PreferenceRepositoryType preferenceRepositoryType = new SharedPreferenceRepository(context); - AccountKeystoreService accountKeystoreService = - new GethKeystoreAccountService(new File(context.getFilesDir(), "store")); - EthereumNetworkRepositoryType networkRepository = - new EthereumNetworkRepository(preferenceRepositoryType); - accountRepository = - new WalletRepository(preferenceRepositoryType, accountKeystoreService, networkRepository); - } - - @Test public void testCreateAccount() { - TestObserver subscription = accountRepository.createWallet(PASS_1) - .test(); - subscription.awaitTerminalEvent(); - subscription.assertComplete(); - assertEquals(subscription.valueCount(), 1); - deleteAccount(subscription.values() - .get(0).address, PASS_1); - } - - @Test public void testImportAccount() { - TestObserver subscriber = accountRepository.importKeystoreToWallet(STORE_1, PASS_1) - .toObservable() - .test(); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - subscriber.assertNoErrors(); - - Assert.assertEquals(subscriber.valueCount(), 1); - Assert.assertEquals(subscriber.values() - .get(0).address, ADDRESS_1); - Assert.assertTrue(subscriber.values() - .get(0) - .sameAddress(ADDRESS_1)); - deleteAccount(ADDRESS_1, PASS_1); - } - - @Test public void testDeleteAccount() { - importAccount(STORE_1, PASS_1); - TestObserver subscriber = accountRepository.deleteWallet(ADDRESS_1, PASS_1) - .test(); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - TestObserver accountListSubscriber = accountList(); - accountListSubscriber.awaitTerminalEvent(); - accountListSubscriber.assertComplete(); - Assert.assertEquals(accountListSubscriber.valueCount(), 1); - Assert.assertEquals(accountListSubscriber.values() - .get(0).length, 0); - } - - @Test public void testExportAccountStore() { - importAccount(STORE_1, PASS_1); - TestObserver subscriber = - accountRepository.exportWallet(new Wallet(ADDRESS_1), PASS_1, PASS_1) - .test(); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - Assert.assertEquals(subscriber.valueCount(), 1); - Log.d("EXPORT_ACC", "Val: " + subscriber.values() - .get(0)); - String val = subscriber.values() - .get(0); - try { - JSONObject json = new JSONObject(val); - Assert.assertTrue(("0x" + json.getString("address")).equalsIgnoreCase(ADDRESS_1)); - } catch (Exception ex) { - ex.printStackTrace(); - } - deleteAccount(ADDRESS_1, PASS_1); - } - - @Test public void testFetchAccounts() { - List createdWallets = new ArrayList<>(); - for (int i = 0; i < 100; i++) { - createdWallets.add(createAccount()); - } - TestObserver subscriber = accountRepository.fetchWallets() - .test(); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - Assert.assertEquals(subscriber.valueCount(), 1); - Assert.assertEquals(subscriber.values() - .get(0).length, 100); - - Wallet[] wallets = subscriber.values() - .get(0); - - for (int i = 0; i < 100; i++) { - Assert.assertTrue(createdWallets.get(i) - .sameAddress(wallets[i].address)); - } - for (Wallet wallet : createdWallets) { - deleteAccount(wallet.address, PASS_1); - } - } - - @Test public void testFindAccount() { - importAccount(STORE_1, PASS_1); - TestObserver subscribe = accountRepository.findWallet(ADDRESS_1) - .test(); - subscribe.awaitTerminalEvent(); - subscribe.assertComplete(); - assertEquals(subscribe.valueCount(), 1); - assertTrue(subscribe.values() - .get(0) - .sameAddress(ADDRESS_1)); - deleteAccount(ADDRESS_1, PASS_1); - } - - @Test public void testSetDefaultAccount() { - Wallet wallet = createAccount(); - TestObserver subscriber = accountRepository.setDefaultWallet(wallet) - .test(); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - - TestObserver defaultSubscriber = accountRepository.getDefaultWallet() - .test(); - defaultSubscriber.awaitTerminalEvent(); - defaultSubscriber.assertComplete(); - assertEquals(defaultSubscriber.valueCount(), 1); - assertTrue(defaultSubscriber.values() - .get(0) - .sameAddress(wallet.address)); - deleteAccount(ADDRESS_1, PASS_1); - } - - @Test public void testGetBalance() { - importAccount(STORE_1, PASS_1); - TestObserver subscriber = accountRepository.balanceInWei(new Wallet(ADDRESS_1)) - .test(); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - Log.d("BALANCE", subscriber.values() - .get(0) - .toString()); - deleteAccount(ADDRESS_1, PASS_1); - } - - private void importAccount(String store, String password) { - TestObserver subscriber = accountRepository.importKeystoreToWallet(store, password) - .toObservable() - .test(); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - } - - private void deleteAccount(String address, String pass) { - TestObserver subscription = accountRepository.deleteWallet(address, pass) - .test(); - subscription.awaitTerminalEvent(); - subscription.assertComplete(); - } - - private TestObserver accountList() { - return accountRepository.fetchWallets() - .test(); - } - - private Wallet createAccount() { - TestObserver subscriber = new TestObserver<>(); - accountRepository.createWallet("1234") - .toObservable() - .subscribe(subscriber); - subscriber.awaitTerminalEvent(); - subscriber.assertComplete(); - Assert.assertEquals(subscriber.valueCount(), 1); - return subscriber.values() - .get(0); - } -} diff --git a/app/src/androidTest/java/com/asf/wallet/WalletTest.java b/app/src/androidTest/java/com/asf/wallet/WalletTest.java deleted file mode 100644 index 625fa202c07..00000000000 --- a/app/src/androidTest/java/com/asf/wallet/WalletTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.asf.wallet; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; -import com.asf.wallet.controller.Controller; -import com.asf.wallet.model.VMAccount; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Instrumentation test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) public class WalletTest { - @Test public void useAppContext() throws Exception { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - } - - @Test public void deleteAccountTest() throws Exception { - Controller controller = Controller.with(InstrumentationRegistry.getTargetContext()); - VMAccount account = controller.createAccount("test password"); - assert (account != null); - - try { - controller.deleteAccount(account.getAddress()); - } catch (Exception e) { - assert (false); - } - - assert (controller.getAccount(account.getAddress()) != null); - } - - @Test public void createAccountTest() throws Exception { - Controller controller = Controller.with(InstrumentationRegistry.getTargetContext()); - VMAccount account = controller.createAccount("test password"); - assert (account != null); - } -} diff --git a/app/src/androidTest/java/com/asf/wallet/views/CreateWalletTest.java b/app/src/androidTest/java/com/asf/wallet/views/CreateWalletTest.java deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/src/androidTest/java/com/asf/wallet/views/ScreengrabTest.java b/app/src/androidTest/java/com/asf/wallet/views/ScreengrabTest.java deleted file mode 100644 index 293b08308a4..00000000000 --- a/app/src/androidTest/java/com/asf/wallet/views/ScreengrabTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.asf.wallet.views; - -import android.support.test.espresso.ViewInteraction; -import android.support.test.rule.ActivityTestRule; -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.LargeTest; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; -import com.asf.wallet.R; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeMatcher; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import tools.fastlane.screengrab.Screengrab; - -import static android.support.test.espresso.Espresso.onView; -import static android.support.test.espresso.action.ViewActions.click; -import static android.support.test.espresso.action.ViewActions.scrollTo; -import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; -import static android.support.test.espresso.matcher.ViewMatchers.withClassName; -import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription; -import static android.support.test.espresso.matcher.ViewMatchers.withId; -import static android.support.test.espresso.matcher.ViewMatchers.withText; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.is; - -@LargeTest @RunWith(AndroidJUnit4.class) public class ScreengrabTest { - - @Rule public ActivityTestRule mActivityTestRule = - new ActivityTestRule<>(SplashActivity.class); - - private static Matcher childAtPosition(final Matcher parentMatcher, - final int position) { - - return new TypeSafeMatcher() { - @Override public void describeTo(Description description) { - description.appendText("Child at position " + position + " in parent "); - parentMatcher.describeTo(description); - } - - @Override public boolean matchesSafely(View view) { - ViewParent parent = view.getParent(); - return parent instanceof ViewGroup && parentMatcher.matches(parent) && view.equals( - ((ViewGroup) parent).getChildAt(position)); - } - }; - } - - @Test public void screengrabTest() { - Screengrab.screenshot("1"); - ViewInteraction appCompatButton = onView(allOf(withId(R.id.skip), withText("SKIP"), - childAtPosition( - allOf(withId(R.id.bottomContainer), childAtPosition(withId(R.id.bottom), 1)), 1), - isDisplayed())); - appCompatButton.perform(click()); - - ViewInteraction appCompatButton2 = onView( - allOf(withId(R.id.import_account_button), withText("Import"), - childAtPosition(childAtPosition(withId(android.R.id.content), 0), 2), isDisplayed())); - appCompatButton2.perform(click()); - - Screengrab.screenshot("2"); - - ViewInteraction appCompatImageButton = onView(allOf(withContentDescription("Navigate up"), - childAtPosition(allOf(withId(R.id.toolbar), - childAtPosition(withClassName(is("android.support.design.widget.AppBarLayout")), 0)), - 1), isDisplayed())); - appCompatImageButton.perform(click()); - - ViewInteraction appCompatButton3 = onView( - allOf(withId(R.id.create_account_button), withText("Create"), - childAtPosition(childAtPosition(withId(android.R.id.content), 0), 0), isDisplayed())); - appCompatButton3.perform(click()); - - ViewInteraction appCompatButton4 = onView( - allOf(withId(R.id.later_button), withText("Do it later"), - childAtPosition(childAtPosition(withId(android.R.id.content), 0), 0), isDisplayed())); - appCompatButton4.perform(click()); - - ViewInteraction appCompatButton5 = onView(allOf(withId(android.R.id.button1), withText("OK"), - childAtPosition(childAtPosition(withId(R.id.buttonPanel), 0), 3))); - appCompatButton5.perform(scrollTo(), click()); - - // Added a sleep statement to match the app's execution delay. - // The recommended way to handle such scenarios is to use Espresso idling resources: - // https://google.github.io/android-testing-support-library/docs/espresso/idling-resource/index.html - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - Screengrab.screenshot("3"); - - ViewInteraction bottomNavigationItemView = onView(allOf(withId(R.id.navigation_send), - childAtPosition(childAtPosition(withId(R.id.navigation), 0), 0), isDisplayed())); - bottomNavigationItemView.perform(click()); - - Screengrab.screenshot("4"); - - ViewInteraction appCompatImageButton2 = onView(allOf(withContentDescription("Navigate up"), - childAtPosition( - allOf(withId(R.id.action_bar), childAtPosition(withId(R.id.action_bar_container), 0)), - 1), isDisplayed())); - appCompatImageButton2.perform(click()); - - // Added a sleep statement to match the app's execution delay. - // The recommended way to handle such scenarios is to use Espresso idling resources: - // https://google.github.io/android-testing-support-library/docs/espresso/idling-resource/index.html - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - ViewInteraction bottomNavigationItemView2 = onView(allOf(withId(R.id.navigation_tokens), - childAtPosition(childAtPosition(withId(R.id.navigation), 0), 2), isDisplayed())); - bottomNavigationItemView2.perform(click()); - - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - Screengrab.screenshot("5"); - - ViewInteraction appCompatImageButton3 = onView(allOf(withContentDescription("Navigate up"), - childAtPosition(allOf(withId(R.id.toolbar), - childAtPosition(withClassName(is("android.support.design.widget.AppBarLayout")), 0)), - 1), isDisplayed())); - appCompatImageButton3.perform(click()); - } -} diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index fefa4d98f79..8c363d3e1ca 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -24,6 +24,7 @@ android:supportsRtl="true" android:testOnly="false" android:theme="@style/AppTheme.NoActionBar" + android:networkSecurityConfig="@xml/network_security_config" > + + + + + + diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 00000000000..97f255276a3 Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 00000000000..69a757908ea Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 00000000000..c045e2d6027 Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 00000000000..cd0ead97b37 Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 00000000000..a3bda96a47a Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/values/booleans.xml b/app/src/debug/res/values/booleans.xml index fb348f0da0a..55344e51920 100644 --- a/app/src/debug/res/values/booleans.xml +++ b/app/src/debug/res/values/booleans.xml @@ -1,4 +1,3 @@ - true \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c0babc1cb4..8f58d681577 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,119 +3,309 @@ xmlns:tools="http://schemas.android.com/tools" package="com.asf.wallet"> - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - + android:name="com.asfoundation.wallet.ui.onboarding.OnboardingActivity" + android:label="" /> - + android:windowSoftInputMode="stateHidden" /> - + android:label="@string/app_name" + android:theme="@style/MaterialAppTheme" /> + android:label="@string/title_activity_settings" /> - + android:name="com.asfoundation.wallet.ui.balance.TransactionDetailActivity" + android:label="" /> + android:label="@string/title_my_address" /> - + android:name="com.asfoundation.wallet.ui.balance.BalanceActivity" + android:label="@string/balance_title" /> + - + android:label="@string/title_activity_send" /> - - - + android:label="@string/title_activity_confirmation" /> - + android:label="@string/title_activity_barcode" /> - - + android:label="@string/title_send_settings" /> - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:launchMode="singleInstance" + android:theme="@style/Theme.AppCompat.Transparent.FitAppWindow"> + + + + + + + + + android:screenOrientation="portrait" /> + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + - + + + + + + + \ No newline at end of file diff --git a/app/src/main/aidl/com/appcoins/advertising/AppCoinsAdvertising.aidl b/app/src/main/aidl/com/appcoins/advertising/AppCoinsAdvertising.aidl new file mode 100644 index 00000000000..0dea3922995 --- /dev/null +++ b/app/src/main/aidl/com/appcoins/advertising/AppCoinsAdvertising.aidl @@ -0,0 +1,26 @@ +package com.appcoins.advertising; + +/** +* The AppCoinsAdvertising provides an interface with the advertising service from the AppCoins +* Wallet. +* +* The calls to this interface with receive a responde with a response code with the following meaning: +* RESULT_OK = 0 - success +* RESULT_SERVICE_UNAVAILABLE = 1 - The network connection is down +* RESULT_CAMPAIGN_UNAVAILABLE = 2 - The campaign is not available +*/ +interface AppCoinsAdvertising { + + + /** + * Provides the campaign ID + * Given the calling package and the currently selected wallet address, this method return a bundle + * with the campaign ID available for thar package name if available for that wallet address + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + * on failures. + * "CAMPAIGN_ID" with a String containing the campaign id + */ + Bundle getAvailableCampaign(); + +} \ No newline at end of file diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png deleted file mode 100644 index 6c25cb17642..00000000000 Binary files a/app/src/main/ic_launcher-web.png and /dev/null differ diff --git a/app/src/main/java/com/asfoundation/wallet/App.java b/app/src/main/java/com/asfoundation/wallet/App.java deleted file mode 100644 index e54dcc82575..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/App.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.asfoundation.wallet; - -import android.app.Activity; -import android.app.Service; -import android.support.multidex.MultiDexApplication; -import android.support.v4.app.Fragment; -import com.asf.wallet.BuildConfig; -import com.asfoundation.wallet.di.DaggerAppComponent; -import com.asfoundation.wallet.interact.AddTokenInteract; -import com.asfoundation.wallet.interact.DefaultTokenProvider; -import com.asfoundation.wallet.poa.ProofOfAttentionService; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; -import com.asfoundation.wallet.repository.WalletNotFoundException; -import com.asfoundation.wallet.ui.iab.AppcoinsOperationsDataSaver; -import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor; -import com.crashlytics.android.Crashlytics; -import com.crashlytics.android.core.CrashlyticsCore; -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.HasActivityInjector; -import dagger.android.HasServiceInjector; -import dagger.android.support.HasSupportFragmentInjector; -import io.fabric.sdk.android.Fabric; -import io.reactivex.exceptions.UndeliverableException; -import io.reactivex.plugins.RxJavaPlugins; -import io.realm.Realm; -import javax.inject.Inject; - -public class App extends MultiDexApplication - implements HasActivityInjector, HasServiceInjector, HasSupportFragmentInjector { - - private static final String TAG = App.class.getSimpleName(); - @Inject DispatchingAndroidInjector dispatchingActivityInjector; - @Inject DispatchingAndroidInjector dispatchingServiceInjector; - @Inject DispatchingAndroidInjector dispatchingFragmentInjector; - @Inject EthereumNetworkRepositoryType ethereumNetworkRepository; - @Inject AddTokenInteract addTokenInteract; - @Inject DefaultTokenProvider defaultTokenProvider; - @Inject ProofOfAttentionService proofOfAttentionService; - @Inject InAppPurchaseInteractor inAppPurchaseInteractor; - @Inject AppcoinsOperationsDataSaver appcoinsOperationsDataSaver; - - @Override public void onCreate() { - super.onCreate(); - Realm.init(this); - DaggerAppComponent.builder() - .application(this) - .build() - .inject(this); - setupRxJava(); - - Fabric.with(this, new Crashlytics.Builder().core( - new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG) - .build()) - .build()); - - inAppPurchaseInteractor.start(); - proofOfAttentionService.start(); - appcoinsOperationsDataSaver.start(); - ethereumNetworkRepository.addOnChangeDefaultNetwork( - networkInfo -> defaultTokenProvider.getDefaultToken() - .flatMapCompletable( - defaultToken -> addTokenInteract.add(defaultToken.address, defaultToken.symbol, - defaultToken.decimals)) - .doOnError(throwable -> { - if (!(throwable instanceof WalletNotFoundException)) { - throwable.printStackTrace(); - } - }) - .retry() - .subscribe()); - } - - private void setupRxJava() { - RxJavaPlugins.setErrorHandler(throwable -> { - if (throwable instanceof UndeliverableException) { - Crashlytics crashlytics = Crashlytics.getInstance(); - if (crashlytics != null && crashlytics.getFabric() - .isDebuggable()) { - Crashlytics.logException(throwable); - } else { - throwable.printStackTrace(); - } - } else { - throw new RuntimeException(throwable); - } - }); - } - - @Override public AndroidInjector activityInjector() { - return dispatchingActivityInjector; - } - - @Override public AndroidInjector serviceInjector() { - return dispatchingServiceInjector; - } - - @Override public AndroidInjector supportFragmentInjector() { - return dispatchingFragmentInjector; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/App.kt b/app/src/main/java/com/asfoundation/wallet/App.kt new file mode 100644 index 00000000000..115df633819 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/App.kt @@ -0,0 +1,179 @@ +package com.asfoundation.wallet + +import androidx.multidex.MultiDexApplication +import cm.aptoide.analytics.AnalyticsManager +import com.appcoins.wallet.appcoins.rewards.AppcoinsRewards +import com.appcoins.wallet.bdsbilling.ProxyService +import com.appcoins.wallet.bdsbilling.WalletService +import com.appcoins.wallet.bdsbilling.repository.BdsApiSecondary +import com.appcoins.wallet.bdsbilling.repository.RemoteRepository.BdsApi +import com.appcoins.wallet.billing.BillingDependenciesProvider +import com.appcoins.wallet.billing.BillingMessagesMapper +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.analytics.AmplitudeAnalytics +import com.asfoundation.wallet.analytics.RakamAnalytics +import com.asfoundation.wallet.di.DaggerAppComponent +import com.asfoundation.wallet.identification.IdsRepository +import com.asfoundation.wallet.logging.FlurryReceiver +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.logging.SentryReceiver +import com.asfoundation.wallet.poa.ProofOfAttentionService +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.support.AlarmManagerBroadcastReceiver +import com.asfoundation.wallet.ui.iab.AppcoinsOperationsDataSaver +import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor +import com.flurry.android.FlurryAgent +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import io.intercom.android.sdk.Intercom +import io.reactivex.exceptions.UndeliverableException +import io.reactivex.plugins.RxJavaPlugins +import io.sentry.Sentry +import io.sentry.android.AndroidSentryClientFactory +import java.util.* +import javax.inject.Inject + +class App : MultiDexApplication(), HasAndroidInjector, BillingDependenciesProvider { + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + + @Inject + lateinit var proofOfAttentionService: ProofOfAttentionService + + @Inject + lateinit var inAppPurchaseInteractor: InAppPurchaseInteractor + + @Inject + lateinit var appcoinsOperationsDataSaver: AppcoinsOperationsDataSaver + + @Inject + lateinit var bdsApi: BdsApi + + @Inject + lateinit var walletService: WalletService + + @Inject + lateinit var proxyService: ProxyService + + @Inject + lateinit var appcoinsRewards: AppcoinsRewards + + @Inject + lateinit var billingMessagesMapper: BillingMessagesMapper + + @Inject + lateinit var bdsapiSecondary: BdsApiSecondary + + @Inject + lateinit var idsRepository: IdsRepository + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var rakamAnalytics: RakamAnalytics + + @Inject + lateinit var amplitudeAnalytics: AmplitudeAnalytics + + @Inject + lateinit var preferencesRepositoryType: PreferencesRepositoryType + + @Inject + lateinit var analyticsManager: AnalyticsManager + + companion object { + private val TAG = App::class.java.name + } + + override fun onCreate() { + super.onCreate() + val appComponent = DaggerAppComponent.builder() + .application(this) + .build() + appComponent.inject(this) + setupRxJava() + val gpsAvailable = checkGooglePlayServices() + if (gpsAvailable.not()) setupSupportNotificationAlarm() + initiateFlurry() + inAppPurchaseInteractor.start() + proofOfAttentionService.start() + appcoinsOperationsDataSaver.start() + appcoinsRewards.start() + rakamAnalytics.start() + amplitudeAnalytics.start() + initiateIntercom() + initiateSentry() + initializeWalletId() + } + + private fun setupRxJava() { + RxJavaPlugins.setErrorHandler { throwable: Throwable -> + if (throwable is UndeliverableException) { + if (BuildConfig.DEBUG) { + throwable.printStackTrace() + } else { + logger.log(TAG, throwable) + } + } else { + throw RuntimeException(throwable) + } + } + } + + private fun checkGooglePlayServices(): Boolean { + val availability = GoogleApiAvailability.getInstance() + return availability.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS + } + + private fun setupSupportNotificationAlarm() { + AlarmManagerBroadcastReceiver.scheduleAlarm(this) + } + + private fun initiateFlurry() { + if (!BuildConfig.DEBUG) { + FlurryAgent.Builder() + .withLogEnabled(false) + .build(this, BuildConfig.FLURRY_APK_KEY) + logger.addReceiver(FlurryReceiver()) + } + } + + private fun initiateSentry() { + Sentry.init(BuildConfig.SENTRY_DSN_KEY, AndroidSentryClientFactory(this)) + logger.addReceiver(SentryReceiver()) + } + + private fun initiateIntercom() { + Intercom.initialize(this, BuildConfig.INTERCOM_API_KEY, BuildConfig.INTERCOM_APP_ID) + + Intercom.client() + .setInAppMessageVisibility(Intercom.Visibility.GONE) + } + + private fun initializeWalletId() { + if (preferencesRepositoryType.getWalletId() == null) { + val id = UUID.randomUUID() + .toString() + preferencesRepositoryType.setWalletId(id) + } + } + + fun analyticsManager() = analyticsManager + + override fun androidInjector() = androidInjector + + override fun supportedVersion() = BuildConfig.BILLING_SUPPORTED_VERSION + + override fun bdsApi() = bdsApi + + override fun walletService() = walletService + + override fun proxyService() = proxyService + + override fun billingMessagesMapper() = billingMessagesMapper + + override fun bdsApiSecondary() = bdsapiSecondary +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/AppGlideModule.kt b/app/src/main/java/com/asfoundation/wallet/AppGlideModule.kt new file mode 100644 index 00000000000..ad88d76d673 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/AppGlideModule.kt @@ -0,0 +1,32 @@ +package com.asfoundation.wallet + +import android.content.Context +import android.os.Build +import com.bumptech.glide.GlideBuilder +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.DecodeFormat +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.module.AppGlideModule +import com.bumptech.glide.request.RequestOptions + + +@GlideModule +class AppGlideModule : AppGlideModule() { + + override fun applyOptions(context: Context, builder: GlideBuilder) { + + var requestOptions = RequestOptions() + val decodeFormat: DecodeFormat + if (Build.VERSION.SDK_INT >= 26) { + decodeFormat = DecodeFormat.PREFER_ARGB_8888 + requestOptions = requestOptions.disallowHardwareConfig() + } else { + decodeFormat = DecodeFormat.PREFER_RGB_565 + } + requestOptions = requestOptions.format(decodeFormat) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + + builder.setDefaultRequestOptions(requestOptions) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/C.java b/app/src/main/java/com/asfoundation/wallet/C.java index 6258a2fbabd..0fd983f439d 100644 --- a/app/src/main/java/com/asfoundation/wallet/C.java +++ b/app/src/main/java/com/asfoundation/wallet/C.java @@ -7,11 +7,7 @@ public abstract class C { public static final int SHARE_REQUEST_CODE = 1003; public static final String ETHEREUM_NETWORK_NAME = "Ethereum"; - public static final String CLASSIC_NETWORK_NAME = "Ethereum Classic"; - public static final String POA_NETWORK_NAME = "POA Network"; - public static final String KOVAN_NETWORK_NAME = "Kovan (Test)"; public static final String ROPSTEN_NETWORK_NAME = "Ropsten (Test)"; - public static final String SOKOL_NETWORK_NAME = "Sokol (Test)"; public static final String ETHEREUM_TIKER = "ethereum"; public static final String POA_TIKER = "poa"; @@ -42,9 +38,6 @@ public abstract class C { public static final String CHANGELLY_REF_ID = "968d4f0f0bf9"; public static final String DONATION_ADDRESS = "0x9f8284ce2cf0c8ce10685f537b1fff418104a317"; - public static final String DEFAULT_GAS_PRICE = "30000000000"; - public static final String DEFAULT_GAS_LIMIT = "90000"; - public static final String DEFAULT_GAS_LIMIT_FOR_TOKENS = "144000"; public static final long GAS_LIMIT_MIN = 21000L; public static final long GAS_LIMIT_MAX = 300000L; public static final long GAS_PRICE_MIN = 1000000000L; diff --git a/app/src/main/java/com/asfoundation/wallet/advertise/Advertising.kt b/app/src/main/java/com/asfoundation/wallet/advertise/Advertising.kt new file mode 100644 index 00000000000..fdafd166fba --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/advertise/Advertising.kt @@ -0,0 +1,31 @@ +package com.asfoundation.wallet.advertise + +import com.asfoundation.wallet.poa.PoaInformationModel +import com.asfoundation.wallet.poa.ProofSubmissionData +import io.reactivex.Single + +interface Advertising { + + fun getCampaign(packageName: String, versionCode: Int): Single + fun hasSeenPoaNotificationTimePassed(): Boolean + fun clearSeenPoaNotification() + fun saveSeenPoaNotification() + + enum class CampaignAvailabilityType { + AVAILABLE, UNAVAILABLE, UNKNOWN_ERROR, NO_INTERNET_CONNECTION, API_ERROR, + PACKAGE_NAME_NOT_FOUND, UPDATE_REQUIRED + } + + fun hasWalletPrepared(chainId: Int, packageName: String, + versionCode: Int): Single + + fun retrievePoaInformation(address: String): Single +} + +data class CampaignDetails(val responseCode: Advertising.CampaignAvailabilityType, + val campaignId: String? = "", val hoursRemaining: Int = 0, + val minutesRemaining: Int = 0) { + + fun hasReachedPoaLimit() = hoursRemaining != 0 || minutesRemaining != 0 +} + diff --git a/app/src/main/java/com/asfoundation/wallet/advertise/AdvertisingService.kt b/app/src/main/java/com/asfoundation/wallet/advertise/AdvertisingService.kt new file mode 100644 index 00000000000..2dd1a48f757 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/advertise/AdvertisingService.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.advertise + +import android.app.NotificationManager +import android.content.Intent +import android.os.IBinder +import androidx.core.app.NotificationCompat +import com.asfoundation.wallet.interact.AutoUpdateInteract +import dagger.android.DaggerService +import javax.inject.Inject +import javax.inject.Named + +class AdvertisingService : DaggerService() { + @Inject + lateinit var campaignInteract: CampaignInteract + @Inject + lateinit var autoUpdateInteract: AutoUpdateInteract + @Inject + lateinit var notificationManager: NotificationManager + @field:[Inject Named("heads_up")] + lateinit var headsUpNotificationBuilder: NotificationCompat.Builder + + override fun onBind(intent: Intent): IBinder { + return AppCoinsAdvertisingBinder(applicationContext.packageManager, campaignInteract, + autoUpdateInteract, notificationManager, headsUpNotificationBuilder, applicationContext) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/advertise/AdvertisingThrowableCodeMapper.kt b/app/src/main/java/com/asfoundation/wallet/advertise/AdvertisingThrowableCodeMapper.kt new file mode 100644 index 00000000000..32d92cd4b3b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/advertise/AdvertisingThrowableCodeMapper.kt @@ -0,0 +1,31 @@ +package com.asfoundation.wallet.advertise + +import retrofit2.HttpException +import java.net.UnknownHostException + +class AdvertisingThrowableCodeMapper { + internal fun map(throwable: Throwable): Advertising.CampaignAvailabilityType { + return when (throwable) { + is HttpException -> { + mapHttpCode(throwable) + } + is UnknownHostException -> { + Advertising.CampaignAvailabilityType.NO_INTERNET_CONNECTION + } + else -> { + throwable.printStackTrace() + Advertising.CampaignAvailabilityType.UNKNOWN_ERROR + } + } + } + + private fun mapHttpCode(throwable: HttpException): Advertising.CampaignAvailabilityType = + when (throwable.code()) { + 404 -> Advertising.CampaignAvailabilityType.PACKAGE_NAME_NOT_FOUND + in 500..599 -> Advertising.CampaignAvailabilityType.API_ERROR + else -> { + throwable.printStackTrace() + Advertising.CampaignAvailabilityType.UNKNOWN_ERROR + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/advertise/AppCoinsAdvertisingBinder.kt b/app/src/main/java/com/asfoundation/wallet/advertise/AppCoinsAdvertisingBinder.kt new file mode 100644 index 00000000000..dc0814d99a4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/advertise/AppCoinsAdvertisingBinder.kt @@ -0,0 +1,103 @@ +package com.asfoundation.wallet.advertise + +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Binder +import android.os.Bundle +import androidx.core.app.NotificationCompat +import com.appcoins.advertising.AppCoinsAdvertising +import com.asf.wallet.R +import com.asfoundation.wallet.interact.AutoUpdateInteract + +internal class AppCoinsAdvertisingBinder( + private val packageManager: PackageManager, + private val campaignInteract: CampaignInteract, + private val autoUpdateInteract: AutoUpdateInteract, + private val notificationManager: NotificationManager, + private val headsUpNotificationBuilder: NotificationCompat.Builder, + private val context: Context) : + AppCoinsAdvertising.Stub() { + + companion object { + internal const val RESULT_OK = 0 // success + internal const val RESULT_SERVICE_UNAVAILABLE = 1 // The network connection is down + internal const val RESULT_CAMPAIGN_UNAVAILABLE = 2 // The campaign is not available + + internal const val RESPONSE_CODE = "RESPONSE_CODE" + internal const val CAMPAIGN_ID = "CAMPAIGN_ID" + } + + override fun getAvailableCampaign(): Bundle { + val uid = Binder.getCallingUid() + val pkg = packageManager.getNameForUid(uid) + val pkgInfo = packageManager.getPackageInfo(pkg ?: "", 0) + return campaignInteract.getCampaign(pkg ?: "", pkgInfo.versionCode) + .doOnSuccess { handleNotificationDisplay(it, pkgInfo) } + .map { mapCampaignDetails(it) } + .blockingGet() + } + + private fun handleNotificationDisplay(campaign: CampaignDetails, pkgInfo: PackageInfo) { + if (campaign.responseCode == Advertising.CampaignAvailabilityType.UPDATE_REQUIRED) { + if (autoUpdateInteract.shouldShowNotification()) { + showUpdateRequiredNotification() + autoUpdateInteract.saveSeenUpdateNotification() + } + } else if (campaign.hasReachedPoaLimit()) { + if (campaignInteract.hasSeenPoaNotificationTimePassed()) { + showNotification(campaign, pkgInfo) + campaignInteract.saveSeenPoaNotification() + } + } else { + campaignInteract.clearSeenPoaNotification() + } + } + + private fun showUpdateRequiredNotification() { + val intent = autoUpdateInteract.buildUpdateIntent() + val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) + notificationManager.notify(WalletPoAService.SERVICE_ID, + headsUpNotificationBuilder.setStyle(NotificationCompat.BigTextStyle() + .setBigContentTitle( + context.getString(R.string.update_wallet_poa_notification_title)) + .bigText(context.getString( + R.string.update_wallet_poa_notification_body))) + .setContentIntent(pendingIntent) + .build()) + } + + private fun showNotification(campaign: CampaignDetails, packageInfo: PackageInfo?) { + val minutesRemaining = "%02d".format(campaign.minutesRemaining) + + val message = context.getString(R.string.notification_poa_limit_reached, + campaign.hoursRemaining.toString(), minutesRemaining) + val notificationBuilder = + headsUpNotificationBuilder.setStyle(NotificationCompat.BigTextStyle() + .bigText(message)) + .setContentText(message) + packageInfo?.let { + notificationBuilder.setContentTitle(packageManager.getApplicationLabel(it.applicationInfo)) + } + notificationManager.notify(WalletPoAService.SERVICE_ID, notificationBuilder.build()) + } + + private fun mapCampaignDetails(details: CampaignDetails): Bundle { + val bundle = Bundle() + bundle.putInt(RESPONSE_CODE, mapCampaignAvailability(details.responseCode)) + bundle.putString(CAMPAIGN_ID, details.campaignId) + return bundle + } + + private fun mapCampaignAvailability(availabilityType: Advertising.CampaignAvailabilityType) + : Int = + when (availabilityType) { + Advertising.CampaignAvailabilityType.AVAILABLE -> RESULT_OK + Advertising.CampaignAvailabilityType.UNAVAILABLE, Advertising.CampaignAvailabilityType.UPDATE_REQUIRED -> RESULT_CAMPAIGN_UNAVAILABLE + else -> RESULT_SERVICE_UNAVAILABLE + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/advertise/CampaignInteract.kt b/app/src/main/java/com/asfoundation/wallet/advertise/CampaignInteract.kt new file mode 100644 index 00000000000..f104e488062 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/advertise/CampaignInteract.kt @@ -0,0 +1,123 @@ +package com.asfoundation.wallet.advertise + +import com.appcoins.wallet.bdsbilling.WalletService +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.interact.AutoUpdateInteract +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.poa.PoaInformationModel +import com.asfoundation.wallet.poa.ProofSubmissionData +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.repository.WalletNotFoundException +import com.asfoundation.wallet.service.Campaign +import com.asfoundation.wallet.service.CampaignService +import com.asfoundation.wallet.service.CampaignStatus +import io.reactivex.Single +import java.net.UnknownHostException + +class CampaignInteract(private val campaignService: CampaignService, + private val walletService: WalletService, + private val autoUpdateInteract: AutoUpdateInteract, + private val errorMapper: AdvertisingThrowableCodeMapper, + private val defaultWalletInteract: FindDefaultWalletInteract, + private val sharedPreferencesRepository: PreferencesRepositoryType) : + Advertising { + + + override fun getCampaign(packageName: String, versionCode: Int): Single { + if (isHardUpdateRequired()) { + return Single.just(CampaignDetails(Advertising.CampaignAvailabilityType.UPDATE_REQUIRED)) + } + return walletService.getWalletOrCreate() + .flatMap { campaignService.getCampaign(it, packageName, versionCode) } + .map { map(it) } + .onErrorReturn { CampaignDetails(errorMapper.map(it)) } + } + + /** + * Checks if the user has seen the Poa notification in the last 12h + **/ + override fun hasSeenPoaNotificationTimePassed(): Boolean { + val savedTime = sharedPreferencesRepository.getPoaNotificationSeenTime() + val currentTime = System.currentTimeMillis() + val timeToShowNextNotificationInMillis = 3600000 * 12 + return currentTime >= savedTime + timeToShowNextNotificationInMillis + } + + override fun clearSeenPoaNotification() { + sharedPreferencesRepository.clearPoaNotificationSeenTime() + } + + override fun saveSeenPoaNotification() { + sharedPreferencesRepository.setPoaNotificationSeenTime(System.currentTimeMillis()) + } + + private fun map(campaign: Campaign) = + if (campaign.campaignStatus == CampaignStatus.AVAILABLE) CampaignDetails( + Advertising.CampaignAvailabilityType.AVAILABLE, + campaign.campaignId) else CampaignDetails( + Advertising.CampaignAvailabilityType.UNAVAILABLE, + campaign.campaignId, campaign.hoursRemaining, campaign.minutesRemaining) + + override fun hasWalletPrepared(chainId: Int, + packageName: String, + versionCode: Int): Single { + if (isHardUpdateRequired()) { + return Single.just( + ProofSubmissionData(ProofSubmissionData.RequirementsStatus.UPDATE_REQUIRED)) + } + if (!isCorrectNetwork(chainId)) { + return if (isKnownNetwork(chainId)) { + Single.just( + ProofSubmissionData(ProofSubmissionData.RequirementsStatus.WRONG_NETWORK)) + } else { + Single.just( + ProofSubmissionData(ProofSubmissionData.RequirementsStatus.UNKNOWN_NETWORK)) + } + } + return defaultWalletInteract.find() + .flatMap { + campaignService.getCampaign(it.address, + packageName, versionCode) + } + .map { + if (isEligible(it.campaignStatus)) { + ProofSubmissionData(ProofSubmissionData.RequirementsStatus.READY) + } else { + ProofSubmissionData(ProofSubmissionData.RequirementsStatus.NOT_ELIGIBLE, + it.hoursRemaining, it.minutesRemaining) + } + } + .onErrorReturn { + when (it) { + is WalletNotFoundException -> ProofSubmissionData( + ProofSubmissionData.RequirementsStatus.NO_WALLET) + is UnknownHostException -> ProofSubmissionData( + ProofSubmissionData.RequirementsStatus.NO_NETWORK) + else -> throw it + } + } + } + + private fun isEligible(campaignStatus: CampaignStatus): Boolean { + return campaignStatus == CampaignStatus.AVAILABLE + } + + private fun isKnownNetwork(chainId: Int): Boolean { + return chainId == 1 || chainId == 3 + } + + private fun isCorrectNetwork(chainId: Int): Boolean { + return chainId == 3 && BuildConfig.DEBUG || chainId == 1 && !BuildConfig.DEBUG + } + + private fun isHardUpdateRequired(): Boolean { + val autoUpdateModel = autoUpdateInteract.getAutoUpdateModel() + .blockingGet() + return autoUpdateInteract.isHardUpdateRequired(autoUpdateModel.blackList, + autoUpdateModel.updateVersionCode, autoUpdateModel.updateMinSdk) + } + + override fun retrievePoaInformation(address: String): Single { + return campaignService.retrievePoaInformation(address) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/advertise/PoAHandshakeReceiver.java b/app/src/main/java/com/asfoundation/wallet/advertise/PoAHandshakeReceiver.java index 9d0547368f4..21a194bb636 100644 --- a/app/src/main/java/com/asfoundation/wallet/advertise/PoAHandshakeReceiver.java +++ b/app/src/main/java/com/asfoundation/wallet/advertise/PoAHandshakeReceiver.java @@ -17,14 +17,13 @@ /** Receiver for the handshake broadcast */ public class PoAHandshakeReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { + @Override public void onReceive(Context context, Intent intent) { Log.d("PoAHandshakeReceiver", "Broadcast received"); Intent serviceIntent = new Intent(context, WalletPoAService.class); serviceIntent.putExtra(PARAM_APP_PACKAGE_NAME, intent.getStringExtra(PARAM_APP_PACKAGE_NAME)); serviceIntent.putExtra(PARAM_APP_SERVICE_NAME, intent.getStringExtra(PARAM_APP_SERVICE_NAME)); - serviceIntent.putExtra(PARAM_NETWORK_ID, intent.getIntExtra(PARAM_NETWORK_ID, 0)); + serviceIntent.putExtra(PARAM_NETWORK_ID, intent.getIntExtra(PARAM_NETWORK_ID, -1)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(serviceIntent); } else { diff --git a/app/src/main/java/com/asfoundation/wallet/advertise/PoaAnalyticsController.kt b/app/src/main/java/com/asfoundation/wallet/advertise/PoaAnalyticsController.kt new file mode 100644 index 00000000000..138c8ad5644 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/advertise/PoaAnalyticsController.kt @@ -0,0 +1,17 @@ +package com.asfoundation.wallet.advertise + +class PoaAnalyticsController(private val poaStartedEventList: MutableList) { + + fun wasStartedEventSent(packageName: String): Boolean { + return poaStartedEventList.contains(packageName) + } + + fun setStartedEventSentFor(packageName: String) { + poaStartedEventList.add(packageName) + } + + fun cleanStateFor(packageName: String) { + poaStartedEventList.remove(packageName) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/advertise/WalletPoAService.java b/app/src/main/java/com/asfoundation/wallet/advertise/WalletPoAService.java index 9df66490b16..44f2d55beef 100644 --- a/app/src/main/java/com/asfoundation/wallet/advertise/WalletPoAService.java +++ b/app/src/main/java/com/asfoundation/wallet/advertise/WalletPoAService.java @@ -1,30 +1,41 @@ package com.asfoundation.wallet.advertise; -import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Messenger; -import android.support.annotation.StringRes; -import android.support.v4.app.NotificationCompat; import android.util.Log; -import com.asf.wallet.BuildConfig; +import androidx.annotation.IntRange; +import androidx.core.app.NotificationCompat; import com.asf.wallet.R; +import com.asfoundation.wallet.billing.analytics.PoaAnalytics; +import com.asfoundation.wallet.interact.AutoUpdateInteract; +import com.asfoundation.wallet.logging.Logger; +import com.asfoundation.wallet.poa.PoaInformationModel; import com.asfoundation.wallet.poa.Proof; import com.asfoundation.wallet.poa.ProofOfAttentionService; import com.asfoundation.wallet.poa.ProofStatus; -import com.asfoundation.wallet.poa.ProofSubmissionFeeData; +import com.asfoundation.wallet.poa.ProofSubmissionData; +import com.asfoundation.wallet.repository.WrongNetworkException; +import com.asfoundation.wallet.ui.TransactionsActivity; +import com.asfoundation.wallet.wallet_validation.dialog.WalletValidationBroadcastReceiver; import dagger.android.AndroidInjection; -import io.reactivex.Single; +import io.reactivex.Observable; import io.reactivex.disposables.Disposable; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; +import javax.inject.Named; import static com.asfoundation.wallet.advertise.ServiceConnector.ACTION_ACK_BROADCAST; import static com.asfoundation.wallet.advertise.ServiceConnector.MSG_REGISTER_CAMPAIGN; @@ -35,6 +46,9 @@ import static com.asfoundation.wallet.advertise.ServiceConnector.PARAM_APP_SERVICE_NAME; import static com.asfoundation.wallet.advertise.ServiceConnector.PARAM_NETWORK_ID; import static com.asfoundation.wallet.advertise.ServiceConnector.PARAM_WALLET_PACKAGE_NAME; +import static com.asfoundation.wallet.wallet_validation.dialog.WalletValidationBroadcastReceiver.ACTION_DISMISS; +import static com.asfoundation.wallet.wallet_validation.dialog.WalletValidationBroadcastReceiver.ACTION_KEY; +import static com.asfoundation.wallet.wallet_validation.dialog.WalletValidationBroadcastReceiver.ACTION_START_VALIDATION; /** * Created by Joao Raimundo on 29/03/2018. @@ -43,50 +57,103 @@ public class WalletPoAService extends Service { public static final int SERVICE_ID = 77784; - static final String TAG = WalletPoAService.class.getSimpleName(); + public static final int VERIFICATION_SERVICE_ID = 77785; + + private static final String TAG = WalletPoAService.class.getSimpleName(); /** * Target we publish for clients to send messages to IncomingHandler.Note * that calls to its binder are sequential! */ final Messenger serviceMessenger = new Messenger(new IncomingHandler()); - - /** Boolean indicating that we are already bound */ - boolean isBound = false; - @Inject ProofOfAttentionService proofOfAttentionService; + @Inject @Named("MAX_NUMBER_PROOF_COMPONENTS") int maxNumberProofComponents; + @Inject Logger logger; + @Inject PoaAnalytics analytics; + @Inject PoaAnalyticsController analyticsController; + @Inject NotificationManager notificationManager; + @Inject PackageManager packageManager; + @Inject CampaignInteract campaignInteract; + @Inject AutoUpdateInteract autoUpdateInteract; + @Inject @Named("heads_up") NotificationCompat.Builder headsUpNotificationBuilder; + /** Boolean indicating that we are already bound */ + private boolean isBound = false; private Disposable disposable; - private NotificationManager notificationManager; + private Disposable timerDisposable; + private Disposable requirementsDisposable; + private Disposable startedEventDisposable; + private Disposable completedEventDisposable; + private String appName; @Override public void onCreate() { super.onCreate(); - notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); AndroidInjection.inject(this); } @Override public int onStartCommand(Intent intent, int flags, int startId) { - startNotifications(); - if (!isBound && intent != null) { - if (intent.hasExtra(PARAM_APP_PACKAGE_NAME)) { + if (intent != null && intent.hasExtra(PARAM_APP_PACKAGE_NAME)) { + startNotifications(); + handlePoaStartToSendEvent(); + handlePoaCompletedToSendEvent(); + + if (!isBound) { // set the chain id received from the application. If not received, it is set as the main - // network chain id - proofOfAttentionService.setChainId( - intent.getStringExtra(PARAM_APP_PACKAGE_NAME), - intent.getIntExtra(PARAM_NETWORK_ID, 1)); - Single.just(intent) - .flatMap(receivedIntent -> proofOfAttentionService.isWalletReady( - intent.getStringExtra(PARAM_APP_PACKAGE_NAME)) - .doOnSuccess( - requirementsStatus -> processWalletSate(requirementsStatus, receivedIntent))) - .subscribe(); + String packageName = intent.getStringExtra(PARAM_APP_PACKAGE_NAME); + try { + ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0); + appName = packageManager.getApplicationLabel(appInfo) + .toString(); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + int versionCode = getVersionCode(packageName); + requirementsDisposable = proofOfAttentionService.handleCreateWallet() + .flatMap(__ -> proofOfAttentionService.isWalletReady( + intent.getIntExtra(PARAM_NETWORK_ID, -1), packageName, versionCode) + // network chain id + .doOnSuccess(proof -> proofOfAttentionService.setChainId(packageName, + intent.getIntExtra(PARAM_NETWORK_ID, -1))) + .doOnSuccess(proof -> processWalletState(proof, intent, packageName))) + .subscribe(requirementsStatus -> { + }, throwable -> { + analytics.sendRakamProofEvent(packageName, "fail", throwable.toString()); + logger.log(TAG, throwable); + showGenericErrorNotificationAndStopForeground(); + }); } + setTimeout(intent.getStringExtra(PARAM_APP_PACKAGE_NAME)); } return super.onStartCommand(intent, flags, startId); } - private void processWalletSate(ProofSubmissionFeeData.RequirementsStatus requirementsStatus, - Intent intent) { - switch (requirementsStatus) { + /** + * When binding to the service, we return an interface to our messenger for + * sending messages to the service. + */ + @Override public IBinder onBind(Intent intent) { + isBound = true; + return serviceMessenger.getBinder(); + } + + @Override public boolean onUnbind(Intent intent) { + isBound = false; + return true; + } + + @Override public void onRebind(Intent intent) { + isBound = true; + super.onRebind(intent); + } + + private void showGenericErrorNotificationAndStopForeground() { + notificationManager.notify(SERVICE_ID, + createDefaultNotificationBuilder(getString(R.string.notification_generic_error)).build()); + stopForeground(false); + stopTimeout(); + } + + private void processWalletState(ProofSubmissionData proof, Intent intent, String packageName) { + switch (proof.getStatus()) { case READY: // send intent to confirm that we receive the broadcast and we want to finish the handshake String appPackageName = intent.getStringExtra(PARAM_APP_PACKAGE_NAME); @@ -103,51 +170,98 @@ private void processWalletSate(ProofSubmissionFeeData.RequirementsStatus require break; case NO_FUNDS: // show notification mentioning that we have no fund to register the PoA - notificationManager.notify(SERVICE_ID, - createNotification(R.string.notification_no_funds_poa)); + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_no_funds_poa)).build()); stopForeground(false); + stopTimeout(); break; case NO_WALLET: // Show notification mentioning that we have no wallet configured on the app - notificationManager.notify(SERVICE_ID, - createNotification(R.string.notification_no_wallet_poa)); + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_no_wallet_poa)).build()); stopForeground(false); + stopTimeout(); break; case NO_NETWORK: // Show notification mentioning that we have no wallet configured on the app - notificationManager.notify(SERVICE_ID, - createNotification(R.string.notification_no_network_poa)); + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_no_network_poa)).build()); stopForeground(false); + stopTimeout(); + break; + case NOT_ELIGIBLE: + //No campaign or already rewarded so there is no need to notify the user of anything + proofOfAttentionService.remove(packageName); + if (proof.hasReachedPoaLimit()) { + if (campaignInteract.hasSeenPoaNotificationTimePassed()) { + showPoaLimitNotification(proof); + campaignInteract.saveSeenPoaNotification(); + stopForeground(false); + } else { + stopForeground(true); + } + } else { + campaignInteract.clearSeenPoaNotification(); + stopForeground(true); + } + stopTimeout(); + break; + case WRONG_NETWORK: + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_wrong_network_poa)).build()); + stopForeground(false); + stopTimeout(); + logger.log(TAG, new Throwable(new WrongNetworkException("Not on the correct network"))); + break; + case UPDATE_REQUIRED: + if (autoUpdateInteract.shouldShowNotification()) { + showUpdateRequiredNotification(); + autoUpdateInteract.saveSeenUpdateNotification(); + } + stopForeground(false); + stopTimeout(); + break; + case UNKNOWN_NETWORK: + logger.log(TAG, new Throwable(new WrongNetworkException("Unknown network"))); break; } } - /** - * When binding to the service, we return an interface to our messenger for - * sending messages to the service. - */ - @Override public IBinder onBind(Intent intent) { - isBound = true; - return serviceMessenger.getBinder(); + private void showUpdateRequiredNotification() { + Intent intent = autoUpdateInteract.buildUpdateIntent(); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); + notificationManager.notify(SERVICE_ID, headsUpNotificationBuilder.setStyle( + new NotificationCompat.BigTextStyle().setBigContentTitle( + getString(R.string.update_wallet_poa_notification_title)) + .bigText(getString(R.string.update_wallet_poa_notification_body))) + .setContentIntent(pendingIntent) + .build()); } - @Override public boolean onUnbind(Intent intent) { - isBound = false; - return true; + private void showPoaLimitNotification(ProofSubmissionData proof) { + String minutes = String.format("%02d", proof.getMinutesRemaining()); + String message = getString(R.string.notification_poa_limit_reached, + String.valueOf(proof.getHoursRemaining()), minutes); + NotificationCompat.Builder builder = + headsUpNotificationBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(message)); + if (appName != null) { + builder.setContentTitle(appName) + .setContentText(message); + } + notificationManager.notify(SERVICE_ID, builder.build()); } - @Override public void onRebind(Intent intent) { - isBound = true; - super.onRebind(intent); + private void stopTimeout() { + disposeDisposable(timerDisposable); } - public void startNotifications() { - startForeground(SERVICE_ID, createNotification(R.string.notification_ongoing_poa)); + private void startNotifications() { + startForeground(SERVICE_ID, + createDefaultNotificationBuilder(getString(R.string.notification_ongoing_poa)).build()); if (disposable == null || disposable.isDisposed()) { disposable = proofOfAttentionService.get() .flatMapIterable(proofs -> proofs) - .distinctUntilChanged(Proof::getProofStatus) - .doOnNext(proof -> updateNotification(proof.getProofStatus())) + .doOnNext(this::updateNotification) .filter(proof -> proof.getProofStatus() .isTerminate()) .doOnNext(proof -> proofOfAttentionService.remove(proof.getPackageName())) @@ -160,46 +274,154 @@ public void startNotifications() { } } - private void updateNotification(ProofStatus status) { - @StringRes int notificationText; - switch (status) { + private void updateNotification(Proof proof) { + switch (proof.getProofStatus()) { case SUBMITTING: - notificationText = R.string.notification_submitting_poa; + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_submitting_poa)).build()); + stopTimeout(); break; case COMPLETED: - notificationText = R.string.notification_completed_poa; - break; - case NO_FUNDS: - notificationText = R.string.notification_no_funds_poa; + PoaInformationModel poaInformation = proofOfAttentionService.retrievePoaInformation() + .blockingGet(); + Intent intent = TransactionsActivity.newIntent(this); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + String completed = getString(R.string.verification_notification_reward_received_body); + if (!poaInformation.hasRemainingPoa()) { + completed = buildNoPoaRemainingString(poaInformation); + } + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); + NotificationCompat.Builder notificationBuilder = headsUpNotificationBuilder.setStyle( + new NotificationCompat.BigTextStyle().bigText(completed)) + .setContentText(completed) + .setContentIntent(pendingIntent); + if (appName != null) { + notificationBuilder.setContentTitle(appName); + } + notificationManager.notify(SERVICE_ID, notificationBuilder.build()); + campaignInteract.clearSeenPoaNotification(); break; case NO_INTERNET: - notificationText = R.string.notification_no_internet_poa; + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_no_network_poa)).build()); break; case GENERAL_ERROR: - notificationText = R.string.notification_error_poa; + notificationManager.notify(SERVICE_ID, + createDefaultNotificationBuilder(getString(R.string.notification_error_poa)).build()); break; case NO_WALLET: - notificationText = R.string.notification_no_wallet_poa; + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_no_wallet_poa)).build()); break; case CANCELLED: - notificationText = R.string.notification_cancelled_poa; + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_cancelled_poa)).build()); + break; + case NOT_AVAILABLE: + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_not_available_poa)).build()); + break; + case NOT_AVAILABLE_ON_COUNTRY: + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_not_available_for_country_poa)).build()); + break; + case ALREADY_REWARDED: + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_already_rewarded_poa)).build()); + break; + case INVALID_DATA: + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_submit_error_poa)).build()); break; default: case PROCESSING: - notificationText = R.string.notification_ongoing_poa; + int progress = calculateProgress(proof); + notificationManager.notify(SERVICE_ID, createDefaultNotificationBuilder( + getString(R.string.notification_ongoing_poa)).setProgress(100, progress, + progress == 0 || progress == 100) + .build()); + break; + case PHONE_NOT_VERIFIED: + stopForeground(true); + notificationManager.cancel(SERVICE_ID); + notificationManager.notify(VERIFICATION_SERVICE_ID, + createVerificationNotification().build()); break; } - notificationManager.notify(SERVICE_ID, createNotification(notificationText)); - if (status.isTerminate()) { + if (proof.getProofStatus() + .isTerminate()) { stopForeground(false); + stopTimeout(); + } + } + + private String buildNoPoaRemainingString(PoaInformationModel poaInformation) { + String minutesRemaining = String.format("%02d", poaInformation.getRemainingMinutes()); + return getString(R.string.notification_completed_poa, + String.valueOf(poaInformation.getRemainingHours()), minutesRemaining); + } + + private @IntRange(from = 0, to = 100) int calculateProgress(Proof proof) { + int progress = 0; + progress += proof.getProofComponentList() + .size(); + if (proof.getCampaignId() != null) { + progress++; + } + if (proof.getStoreAddress() != null) { + progress++; + } + if (proof.getOemAddress() != null) { + progress++; + } + return progress * 100 / (maxNumberProofComponents + 3); + } + + private NotificationCompat.Builder createVerificationNotification() { + NotificationCompat.Builder builder; + String channelId = "notification_channel_verification_id"; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + CharSequence channelName = "Notification Verification Channel"; + int importance = NotificationManager.IMPORTANCE_HIGH; + NotificationChannel notificationChannel = + new NotificationChannel(channelId, channelName, importance); + builder = new NotificationCompat.Builder(this, channelId); + + NotificationManager notificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.createNotificationChannel(notificationChannel); + } else { + builder = new NotificationCompat.Builder(this, channelId); + builder.setVibrate(new long[0]); } + + Intent okIntent = WalletValidationBroadcastReceiver.newIntent(this); + okIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + okIntent.putExtra(ACTION_KEY, ACTION_START_VALIDATION); + PendingIntent okPendingIntent = PendingIntent.getBroadcast(this, 0, okIntent, 0); + + Intent dismissIntent = WalletValidationBroadcastReceiver.newIntent(this); + dismissIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + dismissIntent.putExtra(ACTION_KEY, ACTION_DISMISS); + PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(this, 1, dismissIntent, 0); + + return builder.setContentTitle( + getString(R.string.verification_notification_verification_needed_title)) + .setContentIntent(okPendingIntent) + .setAutoCancel(true) + .setOngoing(true) + .addAction(0, getString(R.string.ok), okPendingIntent) + .addAction(0, getString(R.string.dismiss_button), dismissPendingIntent) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setContentText(getString(R.string.verification_notification_verification_needed_body)); } - private Notification createNotification(int notificationText) { + private NotificationCompat.Builder createDefaultNotificationBuilder(String notificationText) { NotificationCompat.Builder builder; + String channelId = "notification_channel_id"; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - String channelId = "notification_channel_id"; CharSequence channelName = "Notification channel"; int importance = NotificationManager.IMPORTANCE_LOW; NotificationChannel notificationChannel = @@ -210,13 +432,107 @@ private Notification createNotification(int notificationText) { (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.createNotificationChannel(notificationChannel); } else { - builder = new NotificationCompat.Builder(this); + builder = new NotificationCompat.Builder(this, channelId); } - return builder.setContentTitle(getString(R.string.app_name)) - .setSmallIcon(R.drawable.ic_launcher_foreground) - .setContentText(getString(notificationText)) - .build(); + if (appName != null) { + builder.setContentTitle(appName); + } else { + builder.setContentTitle(getString(R.string.app_name)); + } + return builder.setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentText(notificationText); + } + + private void setTimeout(String packageName) { + disposeDisposable(timerDisposable); + timerDisposable = Observable.timer(3, TimeUnit.MINUTES) + .subscribe(__ -> { + disposeDisposable(requirementsDisposable); + proofOfAttentionService.cancel(packageName); + }); + } + + private void disposeDisposable(Disposable disposable) { + if (disposable != null && !disposable.isDisposed()) { + disposable.dispose(); + } + } + + private void handlePoaStartToSendEvent() { + if (startedEventDisposable == null || startedEventDisposable.isDisposed()) { + startedEventDisposable = proofOfAttentionService.get() + .flatMap(proofs -> Observable.fromIterable(proofs) + .filter(this::shouldSendStartEvent)) + .doOnNext(proof -> { + analyticsController.setStartedEventSentFor(proof.getPackageName()); + analytics.sendPoaStartedEvent(proof.getPackageName(), proof.getCampaignId(), + Integer.toString(proof.getChainId())); + }) + .flatMapSingle(proof -> proofOfAttentionService.get() + .firstOrError()) + .filter(List::isEmpty) + .take(1) + .subscribe(); + } + } + + private boolean shouldSendStartEvent(Proof proof) { + return !analyticsController.wasStartedEventSent(proof.getPackageName()) + && proof.getCampaignId() != null + && !proof.getCampaignId() + .isEmpty() + && !proof.getProofComponentList() + .isEmpty() + && proof.getChainId() > 0; + } + + private void handlePoaCompletedToSendEvent() { + if (completedEventDisposable == null || completedEventDisposable.isDisposed()) { + completedEventDisposable = proofOfAttentionService.get() + .flatMapIterable(proofs -> proofs) + .doOnNext(this::handlePoaCompletedAnalytics) + .filter(proof -> proof.getProofStatus() + .isTerminate()) + .doOnNext(proof -> analyticsController.cleanStateFor(proof.getPackageName())) + .flatMapSingle(proof -> proofOfAttentionService.get() + .firstOrError()) + .filter(List::isEmpty) + .take(1) + .subscribe(); + } + } + + private void handlePoaCompletedAnalytics(Proof proof) { + if (proof.getProofStatus() + .equals(ProofStatus.PHONE_NOT_VERIFIED)) { + analytics.sendRakamProofEvent(proof.getPackageName(), "fail", proof.getProofStatus() + .name()); + } else if (proof.getProofStatus() + .isTerminate()) { + if (proof.getProofStatus() + .equals(ProofStatus.COMPLETED)) { + analytics.sendPoaCompletedEvent(proof.getPackageName(), proof.getCampaignId(), + Integer.toString(proof.getChainId())); + analytics.sendRakamProofEvent(proof.getPackageName(), "success", ""); + } else { + analytics.sendRakamProofEvent(proof.getPackageName(), "fail", proof.getProofStatus() + .name()); + } + } + } + + private int getVersionCode(String packageName) { + PackageInfo packageInfo; + int versionCode = -1; + try { + packageInfo = getPackageManager().getPackageInfo(packageName, 0); + versionCode = packageInfo.versionCode; + } catch (PackageManager.NameNotFoundException e) { + logger.log(TAG, new Throwable("Package not found exception")); + e.printStackTrace(); + } + return versionCode; } /** @@ -226,14 +542,15 @@ class IncomingHandler extends Handler { @Override public void handleMessage(Message msg) { String packageName = msg.getData() .getString("packageName"); + setTimeout(packageName); Log.d(TAG, "handleMessage() called with: msg = [" + msg + "] " + ""); switch (msg.what) { case MSG_REGISTER_CAMPAIGN: Log.d(TAG, "MSG_REGISTER_CAMPAIGN"); proofOfAttentionService.setCampaignId(packageName, msg.getData() .getString("campaignId")); - proofOfAttentionService.setOemAddress(packageName, BuildConfig.DEFAULT_OEM_ADREESS); - proofOfAttentionService.setStoreAddress(packageName, BuildConfig.DEFAULT_STORE_ADREESS); + proofOfAttentionService.setOemAddress(packageName); + proofOfAttentionService.setStoreAddress(packageName); break; case MSG_SEND_PROOF: Log.d(TAG, "MSG_SEND_PROOF"); @@ -246,12 +563,11 @@ class IncomingHandler extends Handler { .getInt("networkId")); break; case MSG_STOP_PROCESS: - Log.d(TAG, "MSG_STOP_PROCESS"); - proofOfAttentionService.cancel(packageName); + Log.d(TAG, "Ignoring MSG_STOP_PROCESS message."); break; default: super.handleMessage(msg); } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/AmplitudeAnalytics.kt b/app/src/main/java/com/asfoundation/wallet/analytics/AmplitudeAnalytics.kt new file mode 100644 index 00000000000..42ce7c5f60e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/AmplitudeAnalytics.kt @@ -0,0 +1,65 @@ +package com.asfoundation.wallet.analytics + +import android.annotation.SuppressLint +import android.content.Context +import com.amplitude.api.Amplitude +import com.amplitude.api.Identify +import com.amplitude.api.TrackingOptions +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.identification.IdsRepository +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import io.reactivex.schedulers.Schedulers + +class AmplitudeAnalytics(private val context: Context, private val idsRepository: IdsRepository) : + AnalyticsSetup { + + private val amplitudeClient = Amplitude.getInstance() + private lateinit var entryPoint: String + + override fun setUserId(walletAddress: String) { + amplitudeClient.userId = walletAddress + } + + override fun setGamificationLevel(level: Int) { + val identify = Identify().append(AmplitudeEventLogger.USER_LEVEL, level) + .append(AmplitudeEventLogger.APTOIDE_PACKAGE, BuildConfig.APPLICATION_ID) + .append(AmplitudeEventLogger.VERSION_CODE, BuildConfig.VERSION_CODE) + .append(AmplitudeEventLogger.ENTRY_POINT, + if (entryPoint.isEmpty()) "other" else entryPoint) + .append(AmplitudeEventLogger.HAS_GMS, hasGms()) + + amplitudeClient.identify(identify) + } + + @SuppressLint("CheckResult") + fun start() { + idsRepository.getInstallerPackage(BuildConfig.APPLICATION_ID) + .doOnSuccess { installerPackage -> + val userId = idsRepository.getActiveWalletAddress() + val userLevel = idsRepository.getGamificationLevel() + amplitudeClient.initialize(context, BuildConfig.AMPLITUDE_API_KEY, userId) + .setTrackingOptions(TrackingOptions().disableAdid()) + setAmplitudeSuperProperties(installerPackage, userLevel) + } + .subscribeOn(Schedulers.io()) + .subscribe({}, { it.printStackTrace() }) + } + + private fun setAmplitudeSuperProperties(installerPackage: String, + userLevel: Int) { + entryPoint = if (installerPackage.isEmpty()) "other" else installerPackage + val identify = Identify().append(AmplitudeEventLogger.USER_LEVEL, userLevel) + .append(AmplitudeEventLogger.APTOIDE_PACKAGE, BuildConfig.APPLICATION_ID) + .append(AmplitudeEventLogger.VERSION_CODE, BuildConfig.VERSION_CODE) + .append(AmplitudeEventLogger.ENTRY_POINT, entryPoint) + .append(AmplitudeEventLogger.HAS_GMS, hasGms()) + + amplitudeClient.identify(identify) + } + + private fun hasGms(): Boolean { + return GoogleApiAvailability.getInstance() + .isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/AmplitudeEventLogger.kt b/app/src/main/java/com/asfoundation/wallet/analytics/AmplitudeEventLogger.kt new file mode 100644 index 00000000000..952492682d6 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/AmplitudeEventLogger.kt @@ -0,0 +1,49 @@ +package com.asfoundation.wallet.analytics + +import android.util.Log +import cm.aptoide.analytics.AnalyticsManager +import cm.aptoide.analytics.EventLogger +import com.amplitude.api.Amplitude +import org.json.JSONException +import org.json.JSONObject + +class AmplitudeEventLogger : EventLogger { + + companion object { + const val APTOIDE_PACKAGE = "aptoide_package" + const val VERSION_CODE = "version_code" + const val ENTRY_POINT = "entry_point" + const val USER_LEVEL = "user_level" + const val HAS_GMS = "has_gms" + private const val TAG = "AmplitudeEventLogger" + } + + override fun setup() = Unit + + override fun log(eventName: String?, data: MutableMap?, + action: AnalyticsManager.Action?, context: String?) { + if (data != null) { + Amplitude.getInstance() + .logEvent(eventName, mapToJsonObject(data)) + } else { + Amplitude.getInstance() + .logEvent(eventName) + } + + Log.d(TAG, + "log() called with: eventName = [$eventName], data = [$data], action = [$action], context = [$context]") + } + + private fun mapToJsonObject(data: Map): JSONObject { + val eventData = JSONObject() + + for (entry in data.entries) { + try { + eventData.put(entry.key, entry.value.toString()) + } catch (e: JSONException) { + e.printStackTrace() + } + } + return eventData + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/AnalyticsAPI.java b/app/src/main/java/com/asfoundation/wallet/analytics/AnalyticsAPI.java new file mode 100644 index 00000000000..b51ca79de20 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/AnalyticsAPI.java @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.analytics; + +import cm.aptoide.analytics.AnalyticsManager; +import io.reactivex.Completable; +import retrofit2.http.Body; +import retrofit2.http.POST; +import retrofit2.http.Path; + +public interface AnalyticsAPI { + + @POST("user/addEvent/action={action}/context=WALLET/name={name}") + Completable registerEvent( + @Path("action") AnalyticsManager.Action action, @Path("name") String eventName, @Body AnalyticsBody body); +} diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/AnalyticsBody.java b/app/src/main/java/com/asfoundation/wallet/analytics/AnalyticsBody.java new file mode 100644 index 00000000000..777a6374a29 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/AnalyticsBody.java @@ -0,0 +1,30 @@ +package com.asfoundation.wallet.analytics; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; + +public class AnalyticsBody { + + @JsonProperty("aptoide_vercode") private int appcoinsVercode; + @JsonProperty("aptoide_package") private String + appcoinsPackage; + private Map data; + + public AnalyticsBody(int appcoinsVercode, String appcoinsPackage, Map data) { + this.appcoinsVercode = appcoinsVercode; + this.appcoinsPackage = appcoinsPackage; + this.data = data; + } + + public int getAppcoinsVercode() { + return appcoinsVercode; + } + + public String getAppcoinsPackage() { + return appcoinsPackage; + } + + public Map getData() { + return data; + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/AnalyticsSetup.kt b/app/src/main/java/com/asfoundation/wallet/analytics/AnalyticsSetup.kt new file mode 100644 index 00000000000..c40a7b5cef7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/AnalyticsSetup.kt @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.analytics + +interface AnalyticsSetup { + + fun setUserId(walletAddress: String) + + fun setGamificationLevel(level: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/BackendEventLogger.java b/app/src/main/java/com/asfoundation/wallet/analytics/BackendEventLogger.java new file mode 100644 index 00000000000..a9fb9afd741 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/BackendEventLogger.java @@ -0,0 +1,41 @@ +package com.asfoundation.wallet.analytics; + +import android.util.Log; +import cm.aptoide.analytics.AnalyticsManager; +import cm.aptoide.analytics.EventLogger; +import com.asf.wallet.BuildConfig; +import io.reactivex.schedulers.Schedulers; +import java.util.Map; + +public class BackendEventLogger implements EventLogger { + + private static final String TAG = AnalyticsManager.class.getSimpleName(); + private final AnalyticsAPI api; + + public BackendEventLogger(AnalyticsAPI api) { + this.api = api; + } + + @Override + public void log(String eventName, Map data, AnalyticsManager.Action action, + String context) { + Log.d(TAG, "log() called with: eventName = [" + + eventName + + "], data = [" + + data + + "], action = [" + + action + + "], context = [" + + context + + "]"); + + api.registerEvent(action, eventName, + new AnalyticsBody(BuildConfig.VERSION_CODE, BuildConfig.APPLICATION_ID, data)) + .subscribeOn(Schedulers.io()) + .subscribe(() -> Log.d(TAG, "event sent"), Throwable::printStackTrace); + } + + @Override public void setup() { + Log.d(AnalyticsManager.class.getSimpleName(), "setup() called"); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/FacebookEventLogger.java b/app/src/main/java/com/asfoundation/wallet/analytics/FacebookEventLogger.java new file mode 100644 index 00000000000..d6c1d255198 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/FacebookEventLogger.java @@ -0,0 +1,63 @@ +package com.asfoundation.wallet.analytics; + +import android.os.Bundle; +import android.util.Log; +import cm.aptoide.analytics.AnalyticsManager; +import cm.aptoide.analytics.EventLogger; +import com.asfoundation.wallet.billing.analytics.BillingAnalytics; +import com.facebook.appevents.AppEventsLogger; +import java.math.BigDecimal; +import java.util.Currency; +import java.util.HashMap; +import java.util.Map; + +public class FacebookEventLogger implements EventLogger { + + public static final String EVENT_REVENUE_CURRENCY = "EUR"; + private static final String TAG = AnalyticsManager.class.getSimpleName(); + private final AppEventsLogger eventLogger; + + public FacebookEventLogger(AppEventsLogger eventLogger) { + this.eventLogger = eventLogger; + } + + private Bundle flatten(Map data) { + Bundle bundle = new Bundle(); + for (Map.Entry entry : data.entrySet()) { + if (entry.getValue() + .getClass() + .isInstance(new HashMap())) { + bundle.putAll(flatten((HashMap) entry.getValue())); + } else { + bundle.putString(entry.getKey(), entry.getValue() + .toString()); + } + } + return bundle; + } + + @Override + public void log(String eventName, Map data, AnalyticsManager.Action action, + String context) { + Log.d(TAG, "facebook log() called with: eventName = [" + + eventName + + "], data = [" + + data + + "], action = [" + + action + + "], context = [" + + context + + "]"); + Bundle bundle = flatten(data); + if (eventName.equals(BillingAnalytics.REVENUE)) { + eventLogger.logPurchase(new BigDecimal(bundle.getString("value")), + Currency.getInstance(EVENT_REVENUE_CURRENCY)); + } else { + eventLogger.logEvent(eventName, bundle); + } + } + + @Override public void setup() { + + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/HttpClientKnockLogger.java b/app/src/main/java/com/asfoundation/wallet/analytics/HttpClientKnockLogger.java new file mode 100644 index 00000000000..19a8d02dcf9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/HttpClientKnockLogger.java @@ -0,0 +1,25 @@ +package com.asfoundation.wallet.analytics; + +import cm.aptoide.analytics.KnockEventLogger; +import java.io.IOException; +import okhttp3.OkHttpClient; +import okhttp3.Request; + +public class HttpClientKnockLogger implements KnockEventLogger { + private final OkHttpClient okHttpClient; + + public HttpClientKnockLogger(OkHttpClient okHttpClient) { + this.okHttpClient = okHttpClient; + } + + @Override public void log(String url) { + Request request = new Request.Builder().url(url) + .build(); + try { + okHttpClient.newCall(request) + .execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/KeysNormalizer.java b/app/src/main/java/com/asfoundation/wallet/analytics/KeysNormalizer.java new file mode 100644 index 00000000000..504cad50148 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/KeysNormalizer.java @@ -0,0 +1,24 @@ +package com.asfoundation.wallet.analytics; + +import cm.aptoide.analytics.KeyValueNormalizer; +import java.util.HashMap; +import java.util.Map; + +public class KeysNormalizer implements KeyValueNormalizer { + + @Override public Map normalize(Map data) { + Map normalized = new HashMap<>(); + for (Map.Entry entrySet : data.entrySet()) { + if (entrySet.getValue() != null) { + if (entrySet.getValue() + .getClass() + .isInstance(new HashMap())) { + normalized.put(entrySet.getKey(), normalize((HashMap) entrySet.getValue())); + } else { + normalized.put(entrySet.getKey(), entrySet.getValue()); + } + } + } + return normalized; + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/LogcatAnalyticsLogger.java b/app/src/main/java/com/asfoundation/wallet/analytics/LogcatAnalyticsLogger.java new file mode 100644 index 00000000000..da2c27f75bc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/LogcatAnalyticsLogger.java @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.analytics; + +import android.util.Log; +import cm.aptoide.analytics.AnalyticsLogger; + +public class LogcatAnalyticsLogger implements AnalyticsLogger { + @Override public void logDebug(String tag, String msg) { + Log.d(tag, msg); + } + + @Override public void logWarningDebug(String TAG, String msg) { + Log.w(TAG, msg); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/RakamAnalytics.kt b/app/src/main/java/com/asfoundation/wallet/analytics/RakamAnalytics.kt new file mode 100644 index 00000000000..ebc146dd96f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/RakamAnalytics.kt @@ -0,0 +1,116 @@ +package com.asfoundation.wallet.analytics + +import android.app.Application +import android.content.Context +import android.util.Log +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.identification.IdsRepository +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.logging.RakamReceiver +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import io.rakam.api.Rakam +import io.rakam.api.RakamClient +import io.rakam.api.TrackingOptions +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import org.json.JSONException +import org.json.JSONObject +import java.net.MalformedURLException +import java.net.URL + +class RakamAnalytics(private val context: Context, private val idsRepository: IdsRepository, + private val logger: Logger) : + AnalyticsSetup { + private val rakamClient = Rakam.getInstance() + + companion object { + private val TAG = RakamAnalytics::class.java.simpleName + } + + override fun setUserId(walletAddress: String) { + rakamClient.setUserId(walletAddress) + } + + override fun setGamificationLevel(level: Int) { + val superProperties = rakamClient.superProperties ?: JSONObject() + try { + superProperties.put("user_level", level) + } catch (e: JSONException) { + e.printStackTrace() + } + + rakamClient.superProperties = superProperties + } + + + fun start() { + Single.just(idsRepository.getAndroidId()) + .flatMap { deviceId: String -> startRakam(deviceId) } + .flatMap { rakamClient: RakamClient -> + idsRepository.getInstallerPackage(BuildConfig.APPLICATION_ID) + .flatMap { installerPackage: String -> + Single.just(idsRepository.getGamificationLevel()) + .flatMap { level: Int -> + Single.just(hasGms()) + .flatMap { hasGms: Boolean -> + Single.just(idsRepository.getActiveWalletAddress()) + .doOnSuccess { walletAddress: String -> + setRakamSuperProperties(rakamClient, installerPackage, level, + walletAddress, hasGms) + if (!BuildConfig.DEBUG) { + logger.addReceiver(RakamReceiver()) + } + } + } + } + } + } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + private fun startRakam(deviceId: String): Single { + val instance = Rakam.getInstance() + val options = TrackingOptions() + options.disableAdid() + try { + instance.initialize(context, URL(BuildConfig.RAKAM_BASE_HOST), + BuildConfig.RAKAM_API_KEY) + } catch (e: MalformedURLException) { + Log.e(TAG, "error: ", e) + } + instance.setTrackingOptions(options) + instance.deviceId = deviceId + instance.trackSessionEvents(true) + instance.setLogLevel(Log.VERBOSE) + instance.setEventUploadPeriodMillis(1) + instance.enableLogging(true) + return Single.just(instance) + } + + private fun setRakamSuperProperties(instance: RakamClient, installerPackage: String, + userLevel: Int, + userId: String, hasGms: Boolean) { + val superProperties = instance.superProperties ?: JSONObject() + try { + superProperties.put(RakamEventLogger.APTOIDE_PACKAGE, + BuildConfig.APPLICATION_ID) + superProperties.put(RakamEventLogger.VERSION_CODE, BuildConfig.VERSION_CODE) + superProperties.put(RakamEventLogger.ENTRY_POINT, + if (installerPackage.isEmpty()) "other" else installerPackage) + superProperties.put(RakamEventLogger.USER_LEVEL, userLevel) + superProperties.put(RakamEventLogger.HAS_GMS, hasGms) + } catch (e: JSONException) { + e.printStackTrace() + } + instance.superProperties = superProperties + if (userId.isNotEmpty()) instance.setUserId(userId) + instance.enableForegroundTracking(context.applicationContext as Application) + } + + private fun hasGms(): Boolean { + return GoogleApiAvailability.getInstance() + .isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/RakamEventLogger.kt b/app/src/main/java/com/asfoundation/wallet/analytics/RakamEventLogger.kt new file mode 100644 index 00000000000..55467ff64ed --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/RakamEventLogger.kt @@ -0,0 +1,50 @@ +package com.asfoundation.wallet.analytics + +import android.util.Log +import cm.aptoide.analytics.AnalyticsManager +import cm.aptoide.analytics.EventLogger +import io.rakam.api.Rakam +import org.json.JSONException +import org.json.JSONObject + +class RakamEventLogger : EventLogger { + + companion object { + const val APTOIDE_PACKAGE = "aptoide_package" + const val VERSION_CODE = "version_code" + const val ENTRY_POINT = "entry_point" + const val USER_LEVEL = "user_level" + const val HAS_GMS = "has_gms" + private const val TAG = "RakamEventLogger" + } + + override fun setup() = Unit + + override fun log(eventName: String, data: Map?, + action: AnalyticsManager.Action, context: String) { + if (data != null) { + Rakam.getInstance() + .logEvent(eventName, mapToJsonObject(data)) + } else { + Rakam.getInstance() + .logEvent(eventName) + } + + Log.d(TAG, + "log() called with: eventName = [$eventName], data = [$data], action = [$action], context = [$context]") + + } + + private fun mapToJsonObject(data: Map): JSONObject { + val eventData = JSONObject() + + for (entry in data.entries) { + try { + eventData.put(entry.key, entry.value.toString()) + } catch (e: JSONException) { + e.printStackTrace() + } + } + return eventData + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/gamification/GamificationAnalytics.kt b/app/src/main/java/com/asfoundation/wallet/analytics/gamification/GamificationAnalytics.kt new file mode 100644 index 00000000000..075dc99c10c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/gamification/GamificationAnalytics.kt @@ -0,0 +1,30 @@ +package com.asfoundation.wallet.analytics.gamification + +import cm.aptoide.analytics.AnalyticsManager +import java.util.* + +class GamificationAnalytics(private val analytics: AnalyticsManager) : + GamificationEventSender { + + companion object { + const val GAMIFICATION = "GAMIFICATION" + const val GAMIFICATION_MORE_INFO = "GAMIFICATION_MORE_INFO" + private const val EVENT_USER_LEVEL = "user_level" + private const val WALLET = "WALLET" + + } + + override fun sendMainScreenViewEvent(userLevel: Int) { + val eventData = HashMap() + eventData[EVENT_USER_LEVEL] = userLevel + + analytics.logEvent(eventData, GAMIFICATION, AnalyticsManager.Action.VIEW, WALLET) + } + + override fun sendMoreInfoScreenViewEvent(userLevel: Int) { + val eventData = HashMap() + eventData[EVENT_USER_LEVEL] = userLevel + + analytics.logEvent(eventData, GAMIFICATION_MORE_INFO, AnalyticsManager.Action.VIEW, WALLET) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/analytics/gamification/GamificationEventSender.kt b/app/src/main/java/com/asfoundation/wallet/analytics/gamification/GamificationEventSender.kt new file mode 100644 index 00000000000..0a1070df0eb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/analytics/gamification/GamificationEventSender.kt @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.analytics.gamification + +interface GamificationEventSender { + + fun sendMainScreenViewEvent(userLevel: Int) + + fun sendMoreInfoScreenViewEvent(userLevel: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/backup/BackupBroadcastReceiver.kt b/app/src/main/java/com/asfoundation/wallet/backup/BackupBroadcastReceiver.kt new file mode 100644 index 00000000000..396cd715ca4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/backup/BackupBroadcastReceiver.kt @@ -0,0 +1,66 @@ +package com.asfoundation.wallet.backup + +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import com.asfoundation.wallet.backup.BackupNotificationUtils.NOTIFICATION_SERVICE_ID +import com.asfoundation.wallet.ui.backup.WalletBackupActivity +import dagger.android.AndroidInjection +import dagger.android.DaggerBroadcastReceiver +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import javax.inject.Inject + +class BackupBroadcastReceiver : DaggerBroadcastReceiver(), HasAndroidInjector { + + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + + @Inject + lateinit var backupInteract: BackupInteractContract + + private lateinit var notificationManager: NotificationManager + + companion object { + + private const val WALLET_ADDRESS = "wallet_address" + private const val ACTION = "extra_action" + const val ACTION_BACKUP = "action_backup" + const val ACTION_DISMISS = "action_dismiss" + + @JvmStatic + fun newIntent(context: Context, walletAddress: String, action: String) = + Intent(context, BackupBroadcastReceiver::class.java).apply { + putExtra(WALLET_ADDRESS, walletAddress) + putExtra(ACTION, action) + flags = FLAG_ACTIVITY_NEW_TASK + } + } + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + AndroidInjection.inject(this, context) + notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + notificationManager.cancel(NOTIFICATION_SERVICE_ID) + context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) + + val wallet = intent.getStringExtra(WALLET_ADDRESS) + wallet?.let { + backupInteract.saveDismissSystemNotification(it) + + if (intent.getStringExtra(ACTION) == ACTION_BACKUP) { + val backupIntent = WalletBackupActivity.newIntent(context, it) + .apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + context.startActivity(backupIntent) + } else if (intent.getStringExtra(ACTION) == ACTION_DISMISS) return + } + } + + override fun androidInjector() = androidInjector + +} diff --git a/app/src/main/java/com/asfoundation/wallet/backup/BackupInteract.kt b/app/src/main/java/com/asfoundation/wallet/backup/BackupInteract.kt new file mode 100644 index 00000000000..89502ecbb25 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/backup/BackupInteract.kt @@ -0,0 +1,138 @@ +package com.asfoundation.wallet.backup + +import com.asf.wallet.R +import com.asfoundation.wallet.interact.EmptyNotification +import com.asfoundation.wallet.interact.FetchTransactionsInteract +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.ui.balance.BalanceInteract +import com.asfoundation.wallet.ui.gamification.GamificationInteractor +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.functions.Function4 +import java.math.BigDecimal +import java.util.concurrent.TimeUnit + +class BackupInteract( + private val sharedPreferencesRepository: PreferencesRepositoryType, + private val fetchTransactionsInteract: FetchTransactionsInteract, + private val balanceInteract: BalanceInteract, + private val gamificationInteractor: GamificationInteractor, + private val findDefaultWalletInteract: FindDefaultWalletInteract +) : BackupInteractContract { + + companion object { + private const val DISMISS_PERIOD = 30L + private const val TRANSACTION_COUNT_THRESHOLD = 10 + private const val GAMIFICATION_LEVEL_THRESHOLD = 2 + private const val BALANCE_AMOUNT_THRESHOLD = 10 + private const val PURCHASE_NOTIFICATION_THRESHOLD = 2 + } + + override fun getUnwatchedBackupNotification(): Single { + return findDefaultWalletInteract.find() + .flatMap { wallet -> + getBackupThreshold(wallet.address) + .doOnSuccess { + if (it) sharedPreferencesRepository.setHasShownBackup(wallet.address, it) + } + .map { shouldShow -> + BackupNotification( + R.string.backup_home_notification_title, + R.string.backup_home_notification_body, + R.drawable.ic_backup_notification, + R.string.backup_button, + CardNotificationAction.BACKUP).takeIf { shouldShow } ?: EmptyNotification() + } + } + } + + override fun dismissNotification(): Completable { + return findDefaultWalletInteract.find() + .flatMapCompletable { + Completable.fromAction { + sharedPreferencesRepository.setBackupNotificationSeenTime(it.address, + System.currentTimeMillis()) + } + } + } + + private fun getBackupThreshold(walletAddress: String): Single { + val walletRestoreBackup = sharedPreferencesRepository.isWalletRestoreBackup(walletAddress) + val previouslyShownBackup = sharedPreferencesRepository.hasShownBackup(walletAddress) + return if (walletRestoreBackup) { + Single.just(false) + } else { + Single.zip( + meetsLastDismissConditions(walletAddress), + meetsTransactionsCountConditions(walletAddress), + meetsGamificationConditions(), + meetsBalanceConditions(), + Function4 { dismissPeriodGone, transactions, gamification, balance -> + (previouslyShownBackup || transactions || gamification || balance) && dismissPeriodGone + } + ) + } + } + + private fun meetsBalanceConditions(): Single { + return balanceInteract.requestTokenConversion() + .firstOrError() + .map { it.overallFiat.amount >= BigDecimal(BALANCE_AMOUNT_THRESHOLD) } + .onErrorReturn { false } + } + + private fun meetsGamificationConditions(): Single { + return gamificationInteractor.getUserStats() + .map { it.level + 1 >= GAMIFICATION_LEVEL_THRESHOLD } + .onErrorReturn { false } + } + + private fun meetsTransactionsCountConditions(walletAddress: String): Single { + return fetchTransactionsInteract.fetch(walletAddress) + .doAfterTerminate { fetchTransactionsInteract.stop() } + .map { it.size >= TRANSACTION_COUNT_THRESHOLD } + .firstOrError() + .onErrorReturn { false } + } + + private fun meetsLastDismissConditions(walletAddress: String): Single { + return Single.create { + val savedTime = sharedPreferencesRepository.getBackupNotificationSeenTime(walletAddress) + val currentTime = System.currentTimeMillis() + val result = currentTime >= savedTime + TimeUnit.DAYS.toMillis(DISMISS_PERIOD) + it.onSuccess(result) + } + } + + override fun shouldShowSystemNotification(walletAddress: String): Boolean { + val hasRestoredBackup = sharedPreferencesRepository.isWalletRestoreBackup(walletAddress) + val count = sharedPreferencesRepository.getWalletPurchasesCount(walletAddress) + return if (hasRestoredBackup.not() && count > 0 && count % PURCHASE_NOTIFICATION_THRESHOLD == 0) { + sharedPreferencesRepository.hasDismissedBackupSystemNotification(walletAddress) + .not() + } else { + false + } + } + + override fun updateWalletPurchasesCount(walletAddress: String): Completable { + val hasRestoredBackup = sharedPreferencesRepository.isWalletRestoreBackup(walletAddress) + return if (hasRestoredBackup.not()) { + Single.just(sharedPreferencesRepository.getWalletPurchasesCount(walletAddress)) + .map { it + 1 } + .flatMapCompletable { + sharedPreferencesRepository.incrementWalletPurchasesCount(walletAddress, it) + } + } else { + Completable.complete() + } + } + + override fun saveDismissSystemNotification(walletAddress: String) { + sharedPreferencesRepository.setDismissedBackupSystemNotification(walletAddress) + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/backup/BackupInteractContract.kt b/app/src/main/java/com/asfoundation/wallet/backup/BackupInteractContract.kt new file mode 100644 index 00000000000..aefac4ac598 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/backup/BackupInteractContract.kt @@ -0,0 +1,18 @@ +package com.asfoundation.wallet.backup + +import com.asfoundation.wallet.referrals.CardNotification +import io.reactivex.Completable +import io.reactivex.Single + +interface BackupInteractContract { + + fun getUnwatchedBackupNotification(): Single + + fun dismissNotification(): Completable + + fun shouldShowSystemNotification(walletAddress: String): Boolean + + fun updateWalletPurchasesCount(walletAddress: String): Completable + + fun saveDismissSystemNotification(walletAddress: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/backup/BackupNotification.kt b/app/src/main/java/com/asfoundation/wallet/backup/BackupNotification.kt new file mode 100644 index 00000000000..120ef641df4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/backup/BackupNotification.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.backup + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction + +data class BackupNotification(@StringRes override val title: Int, + @StringRes override val body: Int, + @DrawableRes override val icon: Int, @StringRes + override val positiveButtonText: Int, + override val positiveAction: CardNotificationAction) : + CardNotification(title, body, icon, positiveButtonText, positiveAction) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/backup/BackupNotificationUtils.kt b/app/src/main/java/com/asfoundation/wallet/backup/BackupNotificationUtils.kt new file mode 100644 index 00000000000..d69ecc88f64 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/backup/BackupNotificationUtils.kt @@ -0,0 +1,62 @@ +package com.asfoundation.wallet.backup + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.os.Build +import androidx.core.app.NotificationCompat +import com.asf.wallet.R +import com.asfoundation.wallet.backup.BackupBroadcastReceiver.Companion.ACTION_BACKUP +import com.asfoundation.wallet.backup.BackupBroadcastReceiver.Companion.ACTION_DISMISS + +object BackupNotificationUtils { + + const val NOTIFICATION_SERVICE_ID = 77795 + private const val CHANNEL_ID = "backup_notification_channel_id" + private const val CHANNEL_NAME = "Backup Notification Channel" + + private lateinit var notificationManager: NotificationManager + + fun showBackupNotification(context: Context, walletAddress: String) { + notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val backupIntent = createNotificationBackupIntent(context, walletAddress) + val dismissIntent = createNotificationDismissIntent(context, walletAddress) + val builder: NotificationCompat.Builder + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val importance = NotificationManager.IMPORTANCE_HIGH + val notificationChannel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance) + builder = NotificationCompat.Builder(context, CHANNEL_ID) + notificationManager.createNotificationChannel(notificationChannel) + } else { + builder = NotificationCompat.Builder(context, CHANNEL_ID) + } + + val notification = + builder.setContentTitle(context.getString(R.string.backup_notification_title)) + .setAutoCancel(true) + .setContentIntent(backupIntent) + .addAction(0, context.getString(R.string.dismiss_button), dismissIntent) + .addAction(0, context.getString(R.string.backup_title), backupIntent) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setDeleteIntent(dismissIntent) + .setContentText(context.getString(R.string.backup_notification_body)) + .build() + + notificationManager.notify(NOTIFICATION_SERVICE_ID, notification) + } + + private fun createNotificationBackupIntent(context: Context, + walletAddress: String): PendingIntent { + val intent = BackupBroadcastReceiver.newIntent(context, walletAddress, ACTION_BACKUP) + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + + private fun createNotificationDismissIntent(context: Context, + walletAddress: String): PendingIntent { + val intent = BackupBroadcastReceiver.newIntent(context, walletAddress, ACTION_DISMISS) + return PendingIntent.getBroadcast(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/backup/FileInteractor.kt b/app/src/main/java/com/asfoundation/wallet/backup/FileInteractor.kt new file mode 100644 index 00000000000..59fe14b9016 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/backup/FileInteractor.kt @@ -0,0 +1,139 @@ +package com.asfoundation.wallet.backup + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import androidx.core.content.FileProvider +import androidx.documentfile.provider.DocumentFile +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import io.reactivex.Completable +import io.reactivex.Single +import java.io.* + +class FileInteractor(private val context: Context, + private val contentResolver: ContentResolver, + private val preferencesRepositoryType: PreferencesRepositoryType) { + + private var cachedFile: File? = null + + fun createTmpFile(walletAddress: String, content: String, path: File?): Completable { + val fileName = getDefaultBackupFileName(walletAddress) + + //createTempFile adds numbers in front of filename + if (path == null) return Completable.error(Throwable("Null path")) + val file = File.createTempFile("$fileName-", getDefaultBackupFileExtension(), path) + + val fileOutputStream = FileOutputStream(file, false) + try { + fileOutputStream.write(content.toByteArray()) + cachedFile = file + } catch (e: IOException) { + e.printStackTrace() + return Completable.error(Throwable(e)) + } finally { + fileOutputStream.close() + } + return Completable.complete() + } + + //Use this method for android P and below + fun createAndSaveFile(content: String, path: File?, fileName: String): Completable { + val stringPath = path?.path ?: return Completable.error(Throwable("Null path")) + val directory = File("$stringPath${File.separator}AppcoinsBackup") + directory.mkdirs() + val file = File("$directory${File.separator}$fileName${getDefaultBackupFileExtension()}") + val fileOutputStream = FileOutputStream(file, false) + try { + fileOutputStream.write(content.toByteArray()) + } catch (e: IOException) { + e.printStackTrace() + return Completable.error(Throwable(e)) + } finally { + fileOutputStream.close() + } + return Completable.complete() + } + + //Use this method for Android Q and above + fun createAndSaveFile(content: String, documentFile: DocumentFile, + fileName: String): Completable { + //mimetype anything so that the file has the .bck extension alone. + val file = documentFile.createFile("anything", fileName + getDefaultBackupFileExtension()) + ?: return Completable.error( + Throwable("Error creating file")) + + val outputStream = contentResolver.openOutputStream(file.uri) + try { + outputStream?.run { write(content.toByteArray()) } ?: return Completable.error( + Throwable("Null outputStream")) + } catch (e: IOException) { + e.printStackTrace() + return Completable.error(Throwable(e)) + } finally { + outputStream?.close() + } + return Completable.complete() + } + + fun deleteFile() { + try { + cachedFile?.delete() + cachedFile = null + } catch (e: SecurityException) { + e.printStackTrace() + } + } + + fun getCachedFile(): File? = cachedFile + + //If android Q or above, the user must choose the directory so that the file isn't deleted when the app is uninstall + fun getDownloadPath(): File? { + return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS) + else null + } + + fun getTemporaryPath(): File? { + return context.externalCacheDir + } + + fun getUriFromFile(file: File): Uri { + return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) + } + + fun saveChosenUri(uri: Uri) { + preferencesRepositoryType.saveChosenUri(uri.toString()) + } + + fun readFile(fileUri: Uri?): Single { + if (fileUri == null || fileUri.path == null) { + return Single.error(Throwable("Error retrieving file")) + } else { + val keystore = StringBuilder("") + var reader: BufferedReader? = null + try { + val inputStream = contentResolver.openInputStream(fileUri) + reader = BufferedReader(InputStreamReader(inputStream!!)) + var mLine: String? + while (reader.readLine() + .also { mLine = it } != null) { + keystore.append(mLine) + keystore.append('\n') + } + reader.close() + } catch (e: Exception) { + e.printStackTrace() + reader?.close() + return Single.error(e) + } + return Single.just(keystore.toString()) + } + } + + fun getDefaultBackupFileName(walletAddress: String) = "walletbackup$walletAddress" + + private fun getDefaultBackupFileExtension() = ".bck" +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/backup/NotificationNeeded.kt b/app/src/main/java/com/asfoundation/wallet/backup/NotificationNeeded.kt new file mode 100644 index 00000000000..acc710d3faf --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/backup/NotificationNeeded.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.backup + +data class NotificationNeeded( + val isNeeded: Boolean, + val walletAddress: String +) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/CreditsRemoteRepository.java b/app/src/main/java/com/asfoundation/wallet/billing/CreditsRemoteRepository.java new file mode 100644 index 00000000000..77659707702 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/CreditsRemoteRepository.java @@ -0,0 +1,45 @@ +package com.asfoundation.wallet.billing; + +import com.appcoins.wallet.appcoins.rewards.repository.backend.BackendApi; +import com.appcoins.wallet.bdsbilling.repository.RemoteRepository; +import com.appcoins.wallet.bdsbilling.repository.entity.Transaction; +import io.reactivex.Completable; +import io.reactivex.Single; +import java.math.BigDecimal; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class CreditsRemoteRepository + implements com.appcoins.wallet.appcoins.rewards.repository.RemoteRepository { + private final BackendApi backendApi; + private final RemoteRepository remoteRepository; + + public CreditsRemoteRepository(BackendApi backendApi, RemoteRepository remoteRepository) { + this.backendApi = backendApi; + this.remoteRepository = remoteRepository; + } + + @NotNull @Override + public Single getBalance(@NotNull String address) { + return backendApi.getBalance(address); + } + + @NotNull @Override + public Single pay(@NotNull String walletAddress, @NotNull String signature, + @NotNull BigDecimal amount, @Nullable String origin, @Nullable String sku, + @NotNull String type, @NotNull String developerAddress, @NotNull String storeAddress, + @NotNull String oemAddress, @NotNull String packageName, @Nullable String payload, + @Nullable String callback, @Nullable String orderReference, @Nullable String referrerUrl) { + return remoteRepository.registerAuthorizationProof(origin, type, oemAddress, null, + "appcoins_credits", walletAddress, signature, sku, packageName, amount, developerAddress, + storeAddress, payload, callback, orderReference, referrerUrl); + } + + @NotNull @Override + public Completable sendCredits(@NotNull String toWallet, @NotNull String walletAddress, + @NotNull String signature, @NotNull BigDecimal amount, @NotNull String origin, + @NotNull String type, @NotNull String packageName) { + return remoteRepository.transferCredits(toWallet, origin, type, "appcoins_credits", + walletAddress, signature, packageName, amount); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/billing/Price.java b/app/src/main/java/com/asfoundation/wallet/billing/Price.java new file mode 100644 index 00000000000..466b2ac9310 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/Price.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016. + * Modified by Marcelo Benites on 18/08/2016. + */ + +package com.asfoundation.wallet.billing; + +public class Price { + + private final double amount; + private final String currency; + private final String currencySymbol; + + public Price(double amount, String currency, String currencySymbol) { + this.amount = amount; + this.currency = currency; + this.currencySymbol = currencySymbol; + } + + public String getCurrencySymbol() { + return currencySymbol; + } + + public double getAmount() { + return amount; + } + + public String getCurrency() { + return currency; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressFragment.kt b/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressFragment.kt new file mode 100644 index 00000000000..c1df70709e8 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressFragment.kt @@ -0,0 +1,328 @@ +package com.asfoundation.wallet.billing.address + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.widget.ArrayAdapter +import com.asf.wallet.R +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.ui.iab.IabActivity.Companion.BILLING_ADDRESS_CANCEL_CODE +import com.asfoundation.wallet.ui.iab.IabActivity.Companion.BILLING_ADDRESS_REQUEST_CODE +import com.asfoundation.wallet.ui.iab.IabActivity.Companion.BILLING_ADDRESS_SUCCESS_CODE +import com.asfoundation.wallet.ui.iab.IabView +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.dialog_buy_buttons_payment_methods.* +import kotlinx.android.synthetic.main.fragment_billing_address.* +import kotlinx.android.synthetic.main.layout_billing_address.* +import kotlinx.android.synthetic.main.payment_methods_header.* +import kotlinx.android.synthetic.main.view_purchase_bonus.* +import java.math.BigDecimal +import javax.inject.Inject + +class BillingAddressFragment : DaggerFragment(), BillingAddressView { + + companion object { + + const val BILLING_ADDRESS_MODEL = "billing_address_model" + private const val SKU_DESCRIPTION = "sku_description" + private const val DOMAIN_KEY = "domain" + private const val APPC_AMOUNT_KEY = "appc_amount" + private const val BONUS_KEY = "bonus" + private const val IS_DONATION_KEY = "is_donation" + private const val FIAT_AMOUNT_KEY = "fiat_amount" + private const val FIAT_CURRENCY_KEY = "fiat_currency" + private const val STORE_CARD_KEY = "store_card" + private const val IS_STORED_KEY = "is_stored" + + @JvmStatic + fun newInstance(skuDescription: String, domain: String, appcAmount: BigDecimal, bonus: String, + fiatAmount: BigDecimal, fiatCurrency: String, isDonation: Boolean, + shouldStoreCard: Boolean, isStored: Boolean): BillingAddressFragment { + return BillingAddressFragment().apply { + arguments = Bundle().apply { + putString(SKU_DESCRIPTION, skuDescription) + putString(DOMAIN_KEY, domain) + putString(BONUS_KEY, bonus) + putSerializable(APPC_AMOUNT_KEY, appcAmount) + putSerializable(FIAT_AMOUNT_KEY, fiatAmount) + putString(FIAT_CURRENCY_KEY, fiatCurrency) + putBoolean(IS_DONATION_KEY, isDonation) + putBoolean(STORE_CARD_KEY, shouldStoreCard) + putBoolean(IS_STORED_KEY, isStored) + } + } + } + } + + @Inject + lateinit var formatter: CurrencyFormatUtils + + @Inject + lateinit var logger: Logger + + private lateinit var iabView: IabView + private lateinit var presenter: BillingAddressPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = BillingAddressPresenter(this, CompositeDisposable(), AndroidSchedulers.mainThread()) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_billing_address, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupUi() + presenter.present() + } + + private fun setupUi() { + iabView.unlockRotation() + showButtons() + setHeaderInformation() + showBonus() + setupFieldsListener() + setupStateAdapter() + if (isStored) remember.visibility = GONE + else { + remember.visibility = VISIBLE + remember.isChecked = shouldStoreCard + } + } + + private fun showButtons() { + cancel_button.setText(R.string.back_button) + + if (isDonation) buy_button.setText(R.string.action_donate) + else buy_button.setText(R.string.action_buy) + + buy_button.isEnabled = true + buy_button.visibility = VISIBLE + cancel_button.visibility = VISIBLE + } + + private fun setupFieldsListener() { + address.addTextChangedListener(BillingAddressTextWatcher(address_layout)) + number.addTextChangedListener(BillingAddressTextWatcher(number_layout)) + city.addTextChangedListener(BillingAddressTextWatcher(city_layout)) + zipcode.addTextChangedListener(BillingAddressTextWatcher(zipcode_layout)) + } + + private fun setupStateAdapter() { + val languages = resources.getStringArray(R.array.states) + val adapter = ArrayAdapter(requireContext(), R.layout.item_billing_address_state, languages) + state.setAdapter(adapter) + } + + override fun submitClicks(): Observable { + return RxView.clicks(buy_button) + .filter { validateFields() } + .map { + BillingAddressModel( + address.text.toString(), + city.text.toString(), + zipcode.text.toString(), + state.text.toString(), + country.text.toString(), + number.text.toString(), + remember.isChecked + ) + } + } + + override fun finishSuccess(billingAddressModel: BillingAddressModel) { + val intent = Intent().apply { + putExtra(BILLING_ADDRESS_MODEL, billingAddressModel) + } + targetFragment?.onActivityResult(BILLING_ADDRESS_REQUEST_CODE, BILLING_ADDRESS_SUCCESS_CODE, + intent) + iabView.navigateBack() + } + + override fun cancel() { + targetFragment?.onActivityResult(BILLING_ADDRESS_REQUEST_CODE, BILLING_ADDRESS_CANCEL_CODE, + null) + iabView.navigateBack() + } + + private fun validateFields(): Boolean { + var valid = true + if (address.text.isNullOrEmpty()) { + valid = false + address_layout.error = getString(R.string.error_field_required) + } + + if (number.text.isNullOrEmpty()) { + valid = false + number_layout.error = getString(R.string.error_field_required) + } + + if (city.text.isNullOrEmpty()) { + valid = false + city_layout.error = getString(R.string.error_field_required) + } + + if (zipcode.text.isNullOrEmpty()) { + valid = false + zipcode_layout.error = getString(R.string.error_field_required) + } + + if (state.text.isNullOrEmpty()) { + valid = false + state_layout.error = getString(R.string.error_field_required) + } + + if (country.text.isNullOrEmpty()) { + valid = false + country_layout.error = getString(R.string.error_field_required) + } + + return valid + } + + override fun backClicks() = RxView.clicks(cancel_button) + + private fun setHeaderInformation() { + if (isDonation) { + app_name.text = getString(R.string.item_donation) + app_sku_description.text = getString(R.string.item_donation) + } else { + app_name.text = getApplicationName(domain) + app_sku_description.text = skuDescription + } + try { + app_icon.setImageDrawable(context!!.packageManager + .getApplicationIcon(domain)) + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + } + val appcText = formatter.formatCurrency(appcAmount, WalletCurrency.APPCOINS) + .plus(" " + WalletCurrency.APPCOINS.symbol) + val fiatText = formatter.formatCurrency(fiatAmount, WalletCurrency.FIAT) + .plus(" $fiatCurrency") + fiat_price.text = fiatText + appc_price.text = appcText + fiat_price_skeleton.visibility = GONE + appc_price_skeleton.visibility = GONE + fiat_price.visibility = VISIBLE + appc_price.visibility = VISIBLE + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check(context is IabView) { "billing address fragment must be attached to IAB activity" } + iabView = context + } + + @Throws(PackageManager.NameNotFoundException::class) + private fun getApplicationName(appPackage: String): CharSequence? { + val packageManager = context!!.packageManager + val packageInfo = packageManager.getApplicationInfo(appPackage, 0) + return packageManager.getApplicationLabel(packageInfo) + } + + private fun showBonus() { + if (bonus.isNotEmpty()) { + bonus_layout?.visibility = VISIBLE + bonus_msg?.visibility = VISIBLE + bonus_value?.text = getString(R.string.gamification_purchase_header_part_2, bonus) + } else { + bonus_layout?.visibility = GONE + bonus_msg?.visibility = GONE + } + } + + override fun onDestroyView() { + iabView.enableBack() + presenter.stop() + super.onDestroyView() + } + + private val skuDescription: String by lazy { + if (arguments!!.containsKey(SKU_DESCRIPTION)) { + arguments!!.getString(SKU_DESCRIPTION, "") + } else { + throw IllegalArgumentException("sku description data not found") + } + } + + private val domain: String by lazy { + if (arguments!!.containsKey(DOMAIN_KEY)) { + arguments!!.getString(DOMAIN_KEY, "") + } else { + throw IllegalArgumentException("domain data not found") + } + } + + private val appcAmount: BigDecimal by lazy { + if (arguments!!.containsKey(APPC_AMOUNT_KEY)) { + arguments!!.getSerializable(APPC_AMOUNT_KEY) as BigDecimal + } else { + throw IllegalArgumentException("appc amount data not found") + } + } + + private val bonus: String by lazy { + if (arguments!!.containsKey(BONUS_KEY)) { + arguments!!.getString(BONUS_KEY, "") + } else { + throw IllegalArgumentException("bonus data not found") + } + } + + private val fiatAmount: BigDecimal by lazy { + if (arguments!!.containsKey(FIAT_AMOUNT_KEY)) { + arguments!!.getSerializable(FIAT_AMOUNT_KEY) as BigDecimal + } else { + throw IllegalArgumentException("fiat amount data not found") + } + } + + private val fiatCurrency: String by lazy { + if (arguments!!.containsKey(FIAT_CURRENCY_KEY)) { + arguments!!.getString(FIAT_CURRENCY_KEY, "") + } else { + throw IllegalArgumentException("fiat currency data not found") + } + } + + private val isDonation: Boolean by lazy { + if (arguments!!.containsKey(IS_DONATION_KEY)) { + arguments!!.getBoolean(IS_DONATION_KEY) + } else { + throw IllegalArgumentException("is donation data not found") + } + } + + private val shouldStoreCard: Boolean by lazy { + if (arguments!!.containsKey(STORE_CARD_KEY)) { + arguments!!.getBoolean(STORE_CARD_KEY) + } else { + throw IllegalArgumentException("should store card data not found") + } + } + + private val isStored: Boolean by lazy { + if (arguments!!.containsKey(IS_STORED_KEY)) { + arguments!!.getBoolean(IS_STORED_KEY) + } else { + throw IllegalArgumentException("is stored data not found") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressModel.kt b/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressModel.kt new file mode 100644 index 00000000000..4092c4fcca9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressModel.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.billing.address + +import java.io.Serializable + +data class BillingAddressModel( + val address: String, + val city: String, + val zipcode: String, + val state: String, + val country: String, + val number: String, + val remember: Boolean +) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressPresenter.kt b/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressPresenter.kt new file mode 100644 index 00000000000..0943dcf6e5f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressPresenter.kt @@ -0,0 +1,36 @@ +package com.asfoundation.wallet.billing.address + +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +class BillingAddressPresenter( + private val view: BillingAddressView, + private val disposables: CompositeDisposable, + private val viewScheduler: Scheduler) { + + fun present() { + handleSubmitClicks() + handleBackClicks() + } + + private fun handleSubmitClicks() { + disposables.add( + view.submitClicks() + .subscribeOn(viewScheduler) + .doOnNext { view.finishSuccess(it) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleBackClicks() { + disposables.add( + view.backClicks() + .subscribeOn(viewScheduler) + .doOnNext { view.cancel() } + .subscribe() + ) + } + + fun stop() = disposables.clear() + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressTextWatcher.kt b/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressTextWatcher.kt new file mode 100644 index 00000000000..7ab1c98b79b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressTextWatcher.kt @@ -0,0 +1,18 @@ +package com.asfoundation.wallet.billing.address + +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import com.google.android.material.textfield.TextInputLayout + +class BillingAddressTextWatcher(private val view: TextInputLayout) : TextWatcher { + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit + + override fun afterTextChanged(s: Editable) { + if (!TextUtils.isEmpty(s)) view.error = null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressView.kt b/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressView.kt new file mode 100644 index 00000000000..457506aaca9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/address/BillingAddressView.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.billing.address + +import io.reactivex.Observable + +interface BillingAddressView { + + fun backClicks(): Observable + + fun submitClicks(): Observable + + fun finishSuccess(billingAddressModel: BillingAddressModel) + + fun cancel() + +} diff --git a/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenCardWrapper.kt b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenCardWrapper.kt new file mode 100644 index 00000000000..37d0aeec801 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenCardWrapper.kt @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.billing.adyen + +import com.adyen.checkout.base.model.payments.request.CardPaymentMethod + +data class AdyenCardWrapper( + val cardPaymentMethod: CardPaymentMethod, + val shouldStoreCard: Boolean, + val hasCvc: Boolean, + val supportedShopperInteractions: MutableList) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenComponentResponseModel.kt b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenComponentResponseModel.kt new file mode 100644 index 00000000000..f9c28675651 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenComponentResponseModel.kt @@ -0,0 +1,5 @@ +package com.asfoundation.wallet.billing.adyen + +import org.json.JSONObject + +data class AdyenComponentResponseModel(val details: JSONObject?, val paymentData: String?) diff --git a/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenErrorCodeMapper.kt b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenErrorCodeMapper.kt new file mode 100644 index 00000000000..f12d73a8cdc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenErrorCodeMapper.kt @@ -0,0 +1,49 @@ +package com.asfoundation.wallet.billing.adyen + +import androidx.annotation.StringRes +import com.asf.wallet.R + +class AdyenErrorCodeMapper { + + @StringRes + internal fun map(errorCode: Int): Int { + return when (errorCode) { + DECLINED, BLOCKED_CARD, TRANSACTION_NOT_PERMITTED, REVOCATION_OF_AUTH, DECLINED_NON_GENERIC, ISSUER_SUSPECTED_FRAUD -> R.string.purchase_card_error_general_2 + REFERRAL, ACQUIRER_ERROR, ISSUER_UNAVAILABLE -> R.string.purchase_card_error_general_1 + EXPIRED_CARD -> R.string.purchase_card_error_expired + INVALID_AMOUNT, NOT_ENOUGH_BALANCE, RESTRICTED_CARD -> R.string.purchase_card_error_no_funds + INVALID_CARD_NUMBER -> R.string.purchase_card_error_invalid_details + NOT_SUPPORTED -> R.string.purchase_card_error_not_supported + INCORRECT_ONLINE_PIN, PIN_TRIES_EXCEEDED, NOT_3D_AUTHENTICATED -> R.string.purchase_card_error_security + FRAUD, CANCELLED_DUE_TO_FRAUD -> R.string.purchase_error_fraud_code_20 + else -> R.string.purchase_card_error_title + } + } + + companion object { + + const val DECLINED = 2 + const val REFERRAL = 3 + const val ACQUIRER_ERROR = 4 + const val BLOCKED_CARD = 5 + const val EXPIRED_CARD = 6 + const val INVALID_AMOUNT = 7 + const val INVALID_CARD_NUMBER = 8 + const val ISSUER_UNAVAILABLE = 9 + const val NOT_SUPPORTED = 10 + const val NOT_3D_AUTHENTICATED = 11 + const val NOT_ENOUGH_BALANCE = 12 + const val INCORRECT_ONLINE_PIN = 17 + const val PIN_TRIES_EXCEEDED = 18 + const val FRAUD = 20 + const val CANCELLED_DUE_TO_FRAUD = 22 + const val TRANSACTION_NOT_PERMITTED = 23 + const val CVC_DECLINED = 24 + const val RESTRICTED_CARD = 25 + const val REVOCATION_OF_AUTH = 26 + const val DECLINED_NON_GENERIC = 27 + const val ISSUER_SUSPECTED_FRAUD = 31 + + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenPaymentFragment.kt b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenPaymentFragment.kt new file mode 100644 index 00000000000..ca6498f8a8f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenPaymentFragment.kt @@ -0,0 +1,739 @@ +package com.asfoundation.wallet.billing.adyen + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Typeface +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.LinearLayout +import androidx.annotation.StringRes +import androidx.appcompat.widget.SwitchCompat +import androidx.lifecycle.Observer +import com.adyen.checkout.adyen3ds2.Adyen3DS2Component +import com.adyen.checkout.base.model.paymentmethods.StoredPaymentMethod +import com.adyen.checkout.base.model.payments.response.Action +import com.adyen.checkout.base.ui.view.RoundCornerImageView +import com.adyen.checkout.card.CardComponent +import com.adyen.checkout.card.CardConfiguration +import com.adyen.checkout.core.api.Environment +import com.adyen.checkout.redirect.RedirectComponent +import com.airbnb.lottie.FontAssetDelegate +import com.airbnb.lottie.TextDelegate +import com.appcoins.wallet.bdsbilling.Billing +import com.appcoins.wallet.billing.repository.entity.TransactionData +import com.asf.wallet.BuildConfig +import com.asf.wallet.R +import com.asfoundation.wallet.billing.address.BillingAddressFragment.Companion.BILLING_ADDRESS_MODEL +import com.asfoundation.wallet.billing.address.BillingAddressModel +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.navigator.UriNavigator +import com.asfoundation.wallet.service.ServicesErrorCodeMapper +import com.asfoundation.wallet.ui.iab.FragmentNavigator +import com.asfoundation.wallet.ui.iab.IabActivity.Companion.BILLING_ADDRESS_REQUEST_CODE +import com.asfoundation.wallet.ui.iab.IabActivity.Companion.BILLING_ADDRESS_SUCCESS_CODE +import com.asfoundation.wallet.ui.iab.IabView +import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.KeyboardUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.google.android.material.textfield.TextInputLayout +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.ReplaySubject +import kotlinx.android.synthetic.main.adyen_credit_card_layout.* +import kotlinx.android.synthetic.main.adyen_credit_card_layout.fragment_credit_card_authorization_progress_bar +import kotlinx.android.synthetic.main.adyen_credit_card_pre_selected.* +import kotlinx.android.synthetic.main.dialog_buy_buttons_adyen_error.* +import kotlinx.android.synthetic.main.dialog_buy_buttons_payment_methods.* +import kotlinx.android.synthetic.main.fragment_iab_transaction_completed.* +import kotlinx.android.synthetic.main.iab_error_layout.* +import kotlinx.android.synthetic.main.payment_methods_header.* +import kotlinx.android.synthetic.main.selected_payment_method_cc.* +import kotlinx.android.synthetic.main.support_error_layout.* +import kotlinx.android.synthetic.main.support_error_layout.view.* +import kotlinx.android.synthetic.main.view_purchase_bonus.* +import org.apache.commons.lang3.StringUtils +import java.math.BigDecimal +import javax.inject.Inject + +class AdyenPaymentFragment : DaggerFragment(), AdyenPaymentView { + + @Inject + lateinit var inAppPurchaseInteractor: InAppPurchaseInteractor + + @Inject + lateinit var billing: Billing + + @Inject + lateinit var analytics: BillingAnalytics + + @Inject + lateinit var adyenPaymentInteractor: AdyenPaymentInteractor + + @Inject + lateinit var adyenEnvironment: Environment + + @Inject + lateinit var formatter: CurrencyFormatUtils + + @Inject + lateinit var servicesErrorMapper: ServicesErrorCodeMapper + + @Inject + lateinit var logger: Logger + private lateinit var iabView: IabView + private lateinit var presenter: AdyenPaymentPresenter + private lateinit var cardConfiguration: CardConfiguration + private lateinit var compositeDisposable: CompositeDisposable + private lateinit var redirectComponent: RedirectComponent + private lateinit var adyen3DS2Component: Adyen3DS2Component + private var paymentDataSubject: ReplaySubject? = null + private var paymentDetailsSubject: PublishSubject? = null + private var adyen3DSErrorSubject: PublishSubject? = null + private lateinit var adyenCardNumberLayout: TextInputLayout + private lateinit var adyenExpiryDateLayout: TextInputLayout + private lateinit var adyenSecurityCodeLayout: TextInputLayout + private var adyenCardImageLayout: RoundCornerImageView? = null + private var adyenSaveDetailsSwitch: SwitchCompat? = null + private var isStored = false + private var billingAddressInput: PublishSubject? = null + private var billingAddressModel: BillingAddressModel? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + paymentDataSubject = ReplaySubject.createWithSize(1) + paymentDetailsSubject = PublishSubject.create() + adyen3DSErrorSubject = PublishSubject.create() + billingAddressInput = PublishSubject.create() + val navigator = FragmentNavigator(activity as UriNavigator?, iabView) + compositeDisposable = CompositeDisposable() + presenter = + AdyenPaymentPresenter(this, compositeDisposable, AndroidSchedulers.mainThread(), + Schedulers.io(), RedirectComponent.getReturnUrl(context!!), analytics, domain, origin, + adyenPaymentInteractor, inAppPurchaseInteractor.parseTransaction(transactionData, true), + navigator, paymentType, transactionType, amount, currency, isPreSelected, + AdyenErrorCodeMapper(), servicesErrorMapper, gamificationLevel, formatter, logger) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return if (isPreSelected) { + inflater.inflate(R.layout.adyen_credit_card_pre_selected, container, false) + } else { + inflater.inflate(R.layout.adyen_credit_card_layout, container, false) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupUi() + presenter.present(savedInstanceState) + } + + override fun setup3DSComponent() { + adyen3DS2Component = Adyen3DS2Component.PROVIDER.get(this) + adyen3DS2Component.observe(this, Observer { + paymentDetailsSubject?.onNext(AdyenComponentResponseModel(it.details, it.paymentData)) + }) + adyen3DS2Component.observeErrors(this, Observer { + adyen3DSErrorSubject?.onNext(it.errorMessage) + }) + } + + private fun setupUi() { + setupAdyenLayouts() + setupTransactionCompleteAnimation() + handleBuyButtonText() + if (paymentType == PaymentType.CARD.name) setupCardConfiguration() + + handlePreSelectedView() + handleBonusAnimation() + + showProduct() + } + + override fun finishCardConfiguration( + paymentMethod: com.adyen.checkout.base.model.paymentmethods.PaymentMethod, + isStored: Boolean, forget: Boolean, savedInstance: Bundle?) { + this.isStored = isStored + buy_button.visibility = VISIBLE + cancel_button.visibility = VISIBLE + + handleLayoutVisibility(isStored) + prepareCardComponent(paymentMethod, forget, savedInstance) + setStoredPaymentInformation(isStored) + } + + override fun retrievePaymentData() = paymentDataSubject!! + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.apply { + putString(CARD_NUMBER_KEY, adyenCardNumberLayout.editText?.text.toString()) + putString(EXPIRY_DATE_KEY, adyenExpiryDateLayout.editText?.text.toString()) + putString(CVV_KEY, adyenSecurityCodeLayout.editText?.text.toString()) + putBoolean(SAVE_DETAILS_KEY, adyenSaveDetailsSwitch?.isChecked ?: false) + } + presenter.onSaveInstanceState(outState) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check(context is IabView) { "adyen payment fragment must be attached to IAB activity" } + iabView = context + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == BILLING_ADDRESS_REQUEST_CODE && resultCode == BILLING_ADDRESS_SUCCESS_CODE) { + main_view_pre_selected?.visibility = VISIBLE + main_view?.visibility = VISIBLE + val billingAddressModel = + data!!.getSerializableExtra(BILLING_ADDRESS_MODEL) as BillingAddressModel + this.billingAddressModel = billingAddressModel + billingAddressInput?.onNext(true) + } else { + showMoreMethods() + } + } + + override fun billingAddressInput(): Observable { + return billingAddressInput!! + } + + override fun retrieveBillingAddressData() = billingAddressModel + + override fun getAnimationDuration() = lottie_transaction_success.duration + + override fun showProduct() { + try { + app_icon?.setImageDrawable(context!!.packageManager + .getApplicationIcon(domain)) + app_name?.text = getApplicationName(domain) + } catch (e: Exception) { + e.printStackTrace() + } + app_sku_description?.text = skuDescription + val appcValue = formatter.formatCurrency(appcAmount, WalletCurrency.APPCOINS) + appc_price.text = appcValue.plus(" " + WalletCurrency.APPCOINS.symbol) + } + + override fun showLoading() { + fragment_credit_card_authorization_progress_bar.visibility = VISIBLE + if (isPreSelected) { + payment_methods?.visibility = View.INVISIBLE + } else { + if (bonus.isNotEmpty()) { + bonus_layout.visibility = View.INVISIBLE + bonus_msg.visibility = View.INVISIBLE + } + adyen_card_form.visibility = View.INVISIBLE + change_card_button.visibility = View.INVISIBLE + cancel_button.visibility = View.INVISIBLE + buy_button.visibility = View.INVISIBLE + fiat_price_skeleton.visibility = GONE + appc_price_skeleton.visibility = GONE + } + } + + override fun hideLoadingAndShowView() { + fragment_credit_card_authorization_progress_bar?.visibility = GONE + if (isPreSelected) { + payment_methods?.visibility = VISIBLE + } else { + showBonus() + adyen_card_form.visibility = VISIBLE + cancel_button.visibility = VISIBLE + } + } + + override fun showNetworkError() { + showSpecificError(R.string.notification_no_network_poa) + } + + override fun backEvent(): Observable { + return RxView.clicks(cancel_button) + .mergeWith(iabView.backButtonPress()) + } + + override fun showSuccess() { + iab_activity_transaction_completed.visibility = VISIBLE + fragment_credit_card_authorization_progress_bar?.visibility = GONE + if (isPreSelected) { + main_view?.visibility = GONE + main_view_pre_selected?.visibility = GONE + } else { + fragment_credit_card_authorization_progress_bar.visibility = GONE + credit_card_info.visibility = GONE + lottie_transaction_success.visibility = VISIBLE + fragment_adyen_error?.visibility = GONE + fragment_adyen_error_pre_selected?.visibility = GONE + } + } + + override fun showGenericError() { + showSpecificError(R.string.unknown_error) + } + + override fun showWalletValidation(@StringRes error: Int) = iabView.showWalletValidation(error) + + override fun showBillingAddress(value: BigDecimal, currency: String) { + main_view?.visibility = GONE + main_view_pre_selected?.visibility = GONE + iabView.showBillingAddress(value, currency, bonus, appcAmount, this, + adyenSaveDetailsSwitch?.isChecked ?: true, isStored) + } + + + override fun showSpecificError(@StringRes stringRes: Int) { + fragment_credit_card_authorization_progress_bar?.visibility = GONE + cancel_button?.visibility = GONE + buy_button?.visibility = GONE + payment_methods?.visibility = VISIBLE + bonus_layout_pre_selected?.visibility = GONE + bonus_msg_pre_selected?.visibility = GONE + bonus_layout?.visibility = GONE + bonus_msg?.visibility = GONE + more_payment_methods?.visibility = GONE + adyen_card_form?.visibility = GONE + layout_pre_selected?.visibility = GONE + change_card_button?.visibility = GONE + change_card_button_pre_selected?.visibility = GONE + + error_buttons?.visibility = VISIBLE + dialog_buy_buttons_error?.visibility = VISIBLE + + val message = getString(stringRes) + + fragment_adyen_error?.error_message?.text = message + fragment_adyen_error_pre_selected?.error_message?.text = message + fragment_adyen_error?.visibility = VISIBLE + fragment_adyen_error_pre_selected?.visibility = VISIBLE + } + + override fun showCvvError() { + iabView.unlockRotation() + hideLoadingAndShowView() + if (isStored) { + change_card_button?.visibility = VISIBLE + change_card_button_pre_selected?.visibility = VISIBLE + } + buy_button?.visibility = VISIBLE + buy_button?.isEnabled = false + adyenSecurityCodeLayout.error = getString(R.string.purchase_card_error_CVV) + } + + override fun getMorePaymentMethodsClicks() = RxView.clicks(more_payment_methods) + + override fun showMoreMethods() { + main_view?.let { KeyboardUtils.hideKeyboard(it) } + main_view_pre_selected?.let { KeyboardUtils.hideKeyboard(it) } + iabView.unlockRotation() + iabView.showPaymentMethodsView() + } + + override fun setupRedirectComponent() { + redirectComponent = RedirectComponent.PROVIDER.get(this) + redirectComponent.observe(this, Observer { + paymentDetailsSubject?.onNext(AdyenComponentResponseModel(it.details, it.paymentData)) + }) + } + + + override fun handle3DSAction(action: Action) { + adyen3DS2Component.handleAction(activity!!, action) + } + + override fun onAdyen3DSError(): Observable = adyen3DSErrorSubject!! + + override fun forgetCardClick(): Observable { + return if (change_card_button != null) RxView.clicks(change_card_button) + else RxView.clicks(change_card_button_pre_selected) + } + + override fun showProductPrice(amount: String, currencyCode: String) { + fiat_price.text = "$amount $currencyCode" + fiat_price_skeleton.visibility = GONE + appc_price_skeleton.visibility = GONE + fiat_price.visibility = VISIBLE + appc_price.visibility = VISIBLE + } + + override fun adyenErrorBackClicks() = RxView.clicks(error_back) + + override fun adyenErrorCancelClicks() = RxView.clicks(error_cancel) + + override fun errorDismisses() = RxView.clicks(error_dismiss) + + override fun buyButtonClicked() = RxView.clicks(buy_button) + + override fun close(bundle: Bundle?) = iabView.close(bundle) + + override fun submitUriResult(uri: Uri) = redirectComponent.handleRedirectResponse(uri) + + override fun getPaymentDetails(): Observable = + paymentDetailsSubject!! + + override fun getAdyenSupportLogoClicks() = RxView.clicks(layout_support_logo) + + override fun getAdyenSupportIconClicks() = RxView.clicks(layout_support_icn) + + override fun lockRotation() = iabView.lockRotation() + + override fun hideKeyboard() { + view?.let { KeyboardUtils.hideKeyboard(view) } + } + + private fun setupAdyenLayouts() { + adyenCardNumberLayout = + adyen_card_form_pre_selected?.findViewById(R.id.textInputLayout_cardNumber) + ?: adyen_card_form.findViewById(R.id.textInputLayout_cardNumber) + adyenExpiryDateLayout = + adyen_card_form_pre_selected?.findViewById(R.id.textInputLayout_expiryDate) + ?: adyen_card_form.findViewById(R.id.textInputLayout_expiryDate) + adyenSecurityCodeLayout = + adyen_card_form_pre_selected?.findViewById(R.id.textInputLayout_securityCode) + ?: adyen_card_form.findViewById(R.id.textInputLayout_securityCode) + adyenCardImageLayout = adyen_card_form_pre_selected?.findViewById(R.id.cardBrandLogo_imageView) + ?: adyen_card_form?.findViewById(R.id.cardBrandLogo_imageView) + adyenSaveDetailsSwitch = + adyen_card_form_pre_selected?.findViewById(R.id.switch_storePaymentMethod) + ?: adyen_card_form?.findViewById(R.id.switch_storePaymentMethod) + + adyenCardNumberLayout.editText?.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI + adyenExpiryDateLayout.editText?.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI + adyenSecurityCodeLayout.editText?.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI + + adyenSaveDetailsSwitch?.run { + + val params: LinearLayout.LayoutParams = this.layoutParams as LinearLayout.LayoutParams + params.topMargin = 2 + + layoutParams = params + isChecked = true + textSize = 14f + text = getString(R.string.dialog_credit_card_remember) + } + + val height = resources.getDimensionPixelSize(R.dimen.adyen_text_input_layout_height) + + adyenCardNumberLayout.minimumHeight = height + adyenExpiryDateLayout.minimumHeight = height + adyenSecurityCodeLayout.minimumHeight = height + } + + private fun setupCardConfiguration() { + val cardConfigurationBuilder = + CardConfiguration.Builder(activity as Context, BuildConfig.ADYEN_PUBLIC_KEY) + + cardConfiguration = cardConfigurationBuilder.let { + it.setEnvironment(adyenEnvironment) + it.build() + } + } + + @Throws(PackageManager.NameNotFoundException::class) + private fun getApplicationName(appPackage: String): CharSequence? { + val packageManager = context!!.packageManager + val packageInfo = packageManager.getApplicationInfo(appPackage, 0) + return packageManager.getApplicationLabel(packageInfo) + } + + private fun setupTransactionCompleteAnimation() { + val textDelegate = TextDelegate(lottie_transaction_success) + textDelegate.setText("bonus_value", bonus) + textDelegate.setText("bonus_received", + resources.getString(R.string.gamification_purchase_completed_bonus_received)) + lottie_transaction_success.setTextDelegate(textDelegate) + lottie_transaction_success.setFontAssetDelegate(object : FontAssetDelegate() { + override fun fetchFont(fontFamily: String): Typeface { + return Typeface.create("sans-serif-medium", Typeface.BOLD) + } + }) + } + + private fun showBonus() { + if (bonus.isNotEmpty()) { + bonus_layout?.visibility = VISIBLE + bonus_layout_pre_selected?.visibility = VISIBLE + bonus_msg?.visibility = VISIBLE + bonus_msg_pre_selected?.visibility = VISIBLE + bonus_value.text = getString(R.string.gamification_purchase_header_part_2, bonus) + } else { + bonus_layout?.visibility = GONE + bonus_layout_pre_selected?.visibility = GONE + bonus_msg?.visibility = GONE + bonus_msg_pre_selected?.visibility = GONE + } + } + + private fun handleLayoutVisibility(isStored: Boolean) { + if (isStored) { + adyenCardNumberLayout.visibility = GONE + adyenExpiryDateLayout.visibility = GONE + adyenCardImageLayout?.visibility = GONE + change_card_button?.visibility = VISIBLE + change_card_button_pre_selected?.visibility = VISIBLE + view?.let { KeyboardUtils.showKeyboard(it) } + } else { + adyenCardNumberLayout.visibility = VISIBLE + adyenExpiryDateLayout.visibility = VISIBLE + adyenCardImageLayout?.visibility = VISIBLE + change_card_button?.visibility = GONE + change_card_button_pre_selected?.visibility = GONE + } + + } + + private fun prepareCardComponent( + paymentMethodEntity: com.adyen.checkout.base.model.paymentmethods.PaymentMethod, + forget: Boolean, + savedInstanceState: Bundle?) { + if (forget) viewModelStore.clear() + val cardComponent = CardComponent.PROVIDER.get(this, paymentMethodEntity, cardConfiguration) + if (forget) clearFields() + adyen_card_form_pre_selected?.attach(cardComponent, this) + cardComponent.observe(this, Observer { + adyenSecurityCodeLayout.error = null + if (it != null && it.isValid) { + buy_button?.isEnabled = true + view?.let { view -> KeyboardUtils.hideKeyboard(view) } + it.data.paymentMethod?.let { paymentMethod -> + val hasCvc = !paymentMethod.encryptedSecurityCode.isNullOrEmpty() + val supportedShopperInteractions = + if (paymentMethodEntity is StoredPaymentMethod) paymentMethodEntity.supportedShopperInteractions else emptyList() + paymentDataSubject?.onNext( + AdyenCardWrapper(paymentMethod, adyenSaveDetailsSwitch?.isChecked ?: false, hasCvc, + supportedShopperInteractions)) + } + } else { + buy_button?.isEnabled = false + } + }) + if (!forget) { + getFieldValues(savedInstanceState) + } + } + + private fun getFieldValues(savedInstanceState: Bundle?) { + savedInstanceState?.let { + adyenCardNumberLayout.editText?.setText(it.getString(CARD_NUMBER_KEY, "")) + adyenExpiryDateLayout.editText?.setText(it.getString(EXPIRY_DATE_KEY, "")) + adyenSecurityCodeLayout.editText?.setText(it.getString(CVV_KEY, "")) + adyenSaveDetailsSwitch?.isChecked = it.getBoolean(SAVE_DETAILS_KEY, false) + it.clear() + } + } + + private fun setStoredPaymentInformation(isStored: Boolean) { + if (isStored) { + adyen_card_form_pre_selected_number?.text = adyenCardNumberLayout.editText?.text + adyen_card_form_pre_selected_number?.visibility = VISIBLE + payment_method_ic?.setImageDrawable(adyenCardImageLayout?.drawable) + } else { + adyen_card_form_pre_selected_number?.visibility = GONE + payment_method_ic?.visibility = GONE + } + } + + private fun clearFields() { + adyenCardNumberLayout.editText?.text = null + adyenCardNumberLayout.editText?.isEnabled = true + adyenExpiryDateLayout.editText?.text = null + adyenExpiryDateLayout.editText?.isEnabled = true + adyenSecurityCodeLayout.editText?.text = null + adyenCardNumberLayout.requestFocus() + adyenSecurityCodeLayout.error = null + } + + private fun handleBonusAnimation() { + if (StringUtils.isNotBlank(bonus)) { + lottie_transaction_success.setAnimation(R.raw.transaction_complete_bonus_animation) + setupTransactionCompleteAnimation() + } else { + lottie_transaction_success.setAnimation(R.raw.success_animation) + } + } + + private fun handlePreSelectedView() { + if (!isPreSelected) { + cancel_button.setText(R.string.back_button) + iabView.disableBack() + } + showBonus() + } + + private fun handleBuyButtonText() { + if (transactionType.equals(TransactionData.TransactionType.DONATION.name, ignoreCase = true)) { + buy_button.setText(R.string.action_donate) + } else { + buy_button.setText(R.string.action_buy) + } + } + + override fun onDestroyView() { + iabView.enableBack() + presenter.stop() + super.onDestroyView() + } + + override fun onDestroy() { + paymentDataSubject = null + paymentDetailsSubject = null + adyen3DSErrorSubject = null + billingAddressInput = null + super.onDestroy() + } + + companion object { + + private const val TRANSACTION_TYPE_KEY = "type" + private const val PAYMENT_TYPE_KEY = "payment_type" + private const val DOMAIN_KEY = "domain" + private const val ORIGIN_KEY = "origin" + private const val TRANSACTION_DATA_KEY = "transaction_data" + private const val APPC_AMOUNT_KEY = "appc_amount" + private const val AMOUNT_KEY = "amount" + private const val CURRENCY_KEY = "currency" + private const val BONUS_KEY = "bonus" + private const val PRE_SELECTED_KEY = "pre_selected" + private const val CARD_NUMBER_KEY = "card_number" + private const val EXPIRY_DATE_KEY = "expiry_date" + private const val CVV_KEY = "cvv_key" + private const val SAVE_DETAILS_KEY = "save_details" + private const val GAMIFICATION_LEVEL = "gamification_level" + private const val SKU_DESCRIPTION = "sku_description" + + @JvmStatic + fun newInstance(transactionType: String, paymentType: PaymentType, domain: String, + origin: String?, transactionData: String?, appcAmount: BigDecimal, + amount: BigDecimal, currency: String?, bonus: String?, + isPreSelected: Boolean, gamificationLevel: Int, + skuDescription: String): AdyenPaymentFragment { + val fragment = AdyenPaymentFragment() + fragment.arguments = Bundle().apply { + putString(TRANSACTION_TYPE_KEY, transactionType) + putString(PAYMENT_TYPE_KEY, paymentType.name) + putString(DOMAIN_KEY, domain) + putString(ORIGIN_KEY, origin) + putString(TRANSACTION_DATA_KEY, transactionData) + putSerializable(APPC_AMOUNT_KEY, appcAmount) + putSerializable(AMOUNT_KEY, amount) + putString(CURRENCY_KEY, currency) + putString(BONUS_KEY, bonus) + putBoolean(PRE_SELECTED_KEY, isPreSelected) + putInt(GAMIFICATION_LEVEL, gamificationLevel) + putString(SKU_DESCRIPTION, skuDescription) + } + return fragment + } + } + + private val transactionType: String by lazy { + if (arguments!!.containsKey(TRANSACTION_TYPE_KEY)) { + arguments!!.getString(TRANSACTION_TYPE_KEY, "") + } else { + throw IllegalArgumentException("transaction type data not found") + } + } + + private val paymentType: String by lazy { + if (arguments!!.containsKey(PAYMENT_TYPE_KEY)) { + arguments!!.getString(PAYMENT_TYPE_KEY, "") + } else { + throw IllegalArgumentException("payment type data not found") + } + } + + private val domain: String by lazy { + if (arguments!!.containsKey(DOMAIN_KEY)) { + arguments!!.getString(DOMAIN_KEY, "") + } else { + throw IllegalArgumentException("domain data not found") + } + } + + private val origin: String? by lazy { + if (arguments!!.containsKey(ORIGIN_KEY)) { + arguments!!.getString(ORIGIN_KEY) + } else { + throw IllegalArgumentException("origin not found") + } + } + + private val transactionData: String by lazy { + if (arguments!!.containsKey(TRANSACTION_DATA_KEY)) { + arguments!!.getString(TRANSACTION_DATA_KEY, "") + } else { + throw IllegalArgumentException("transaction data not found") + } + } + + private val appcAmount: BigDecimal by lazy { + if (arguments!!.containsKey(APPC_AMOUNT_KEY)) { + arguments!!.getSerializable(APPC_AMOUNT_KEY) as BigDecimal + } else { + throw IllegalArgumentException("appc amount data not found") + } + } + + private val amount: BigDecimal by lazy { + if (arguments!!.containsKey(AMOUNT_KEY)) { + arguments!!.getSerializable(AMOUNT_KEY) as BigDecimal + } else { + throw IllegalArgumentException("amount data not found") + } + } + + private val currency: String by lazy { + if (arguments!!.containsKey(CURRENCY_KEY)) { + arguments!!.getString(CURRENCY_KEY, "") + } else { + throw IllegalArgumentException("currency data not found") + } + } + + private val bonus: String by lazy { + if (arguments!!.containsKey(BONUS_KEY)) { + arguments!!.getString(BONUS_KEY, "") + } else { + throw IllegalArgumentException("bonus data not found") + } + } + + private val isPreSelected: Boolean by lazy { + if (arguments!!.containsKey(PRE_SELECTED_KEY)) { + arguments!!.getBoolean(PRE_SELECTED_KEY) + } else { + throw IllegalArgumentException("pre selected data not found") + } + } + + private val gamificationLevel: Int by lazy { + if (arguments!!.containsKey(GAMIFICATION_LEVEL)) { + arguments!!.getInt(GAMIFICATION_LEVEL) + } else { + throw IllegalArgumentException("gamification level data not found") + } + } + + private val skuDescription: String by lazy { + if (arguments!!.containsKey(SKU_DESCRIPTION)) { + arguments!!.getString(SKU_DESCRIPTION, "") + } else { + throw IllegalArgumentException("sku description data not found") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenPaymentInteractor.kt b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenPaymentInteractor.kt new file mode 100644 index 00000000000..1658de749d7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenPaymentInteractor.kt @@ -0,0 +1,193 @@ +package com.asfoundation.wallet.billing.adyen + +import android.os.Bundle +import com.adyen.checkout.core.model.ModelObject +import com.appcoins.wallet.bdsbilling.Billing +import com.appcoins.wallet.bdsbilling.WalletService +import com.appcoins.wallet.billing.BillingMessagesMapper +import com.appcoins.wallet.billing.adyen.* +import com.appcoins.wallet.billing.util.Error +import com.asfoundation.wallet.billing.partners.AddressService +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.functions.BiFunction +import io.reactivex.schedulers.Schedulers +import org.json.JSONObject +import java.util.* +import java.util.concurrent.TimeUnit + +class AdyenPaymentInteractor( + private val adyenPaymentRepository: AdyenPaymentRepository, + private val inAppPurchaseInteractor: InAppPurchaseInteractor, + private val billingMessagesMapper: BillingMessagesMapper, + private val partnerAddressService: AddressService, + private val billing: Billing, + private val walletService: WalletService, + private val supportInteractor: SupportInteractor, + private val walletBlockedInteract: WalletBlockedInteract, + private val smsValidationInteract: SmsValidationInteract +) { + + fun isWalletBlocked() = walletBlockedInteract.isWalletBlocked() + + fun isWalletVerified() = + walletService.getWalletAddress() + .flatMap { smsValidationInteract.isValidated(it) } + .onErrorReturn { true } + + + fun showSupport(gamificationLevel: Int): Completable { + return walletService.getWalletAddress() + .flatMapCompletable { + Completable.fromAction { + supportInteractor.registerUser(gamificationLevel, it.toLowerCase(Locale.ROOT)) + supportInteractor.displayChatScreen() + } + } + } + + fun loadPaymentInfo(methods: AdyenPaymentRepository.Methods, value: String, + currency: String): Single { + return walletService.getWalletAddress() + .flatMap { adyenPaymentRepository.loadPaymentInfo(methods, value, currency, it) } + } + + fun makePayment(adyenPaymentMethod: ModelObject, shouldStoreMethod: Boolean, hasCvc: Boolean, + supportedShopperInteraction: List, + returnUrl: String, value: String, currency: String, reference: String?, + paymentType: String, origin: String?, packageName: String, metadata: String?, + sku: String?, callbackUrl: String?, transactionType: String, + developerWallet: String?, + billingAddress: AdyenBillingAddress? = null): Single { + return walletService.getAndSignCurrentWalletAddress() + .flatMap { address -> + Single.zip( + partnerAddressService.getStoreAddressForPackage(packageName), + partnerAddressService.getOemAddressForPackage(packageName), + BiFunction { storeAddress: String, oemAddress: String -> + Pair(storeAddress, oemAddress) + }) + .flatMap { + adyenPaymentRepository.makePayment(adyenPaymentMethod, shouldStoreMethod, hasCvc, + supportedShopperInteraction, returnUrl, value, currency, reference, paymentType, + address.address, origin, packageName, metadata, sku, callbackUrl, + transactionType, developerWallet, it.first, it.second, address.address, + address.signedAddress, billingAddress) + } + } + } + + fun makeTopUpPayment(adyenPaymentMethod: ModelObject, shouldStoreMethod: Boolean, hasCvc: Boolean, + supportedShopperInteraction: List, returnUrl: String, value: String, + currency: String, paymentType: String, transactionType: String, + packageName: String, + billingAddress: AdyenBillingAddress? = null): Single { + return walletService.getAndSignCurrentWalletAddress() + .flatMap { + adyenPaymentRepository.makePayment(adyenPaymentMethod, shouldStoreMethod, hasCvc, + supportedShopperInteraction, returnUrl, value, currency, null, paymentType, + it.address, null, packageName, null, null, null, transactionType, null, null, null, + null, it.signedAddress, billingAddress) + } + } + + fun submitRedirect(uid: String, details: JSONObject, + paymentData: String?): Single { + return walletService.getAndSignCurrentWalletAddress() + .flatMap { + adyenPaymentRepository.submitRedirect(uid, it.address, it.signedAddress, details, + paymentData) + } + } + + fun disablePayments(): Single { + return walletService.getWalletAddress() + .flatMap { adyenPaymentRepository.disablePayments(it) } + } + + fun convertToFiat(amount: Double, currency: String): Single { + return inAppPurchaseInteractor.convertToFiat(amount, currency) + } + + fun mapCancellation(): Bundle { + return billingMessagesMapper.mapCancellation() + } + + fun removePreSelectedPaymentMethod() { + inAppPurchaseInteractor.removePreSelectedPaymentMethod() + } + + fun getCompletePurchaseBundle(type: String, merchantName: String, sku: String?, + orderReference: String?, hash: String?, + scheduler: Scheduler): Single { + return if (isInApp(type) && sku != null) { + billing.getSkuPurchase(merchantName, sku, scheduler) + .map { billingMessagesMapper.mapPurchase(it, orderReference) } + } else { + Single.just(billingMessagesMapper.successBundle(hash)) + } + } + + fun convertToLocalFiat(doubleValue: Double): Single { + return inAppPurchaseInteractor.convertToLocalFiat(doubleValue) + } + + fun getAuthorisedTransaction(uid: String): Observable { + return walletService.getAndSignCurrentWalletAddress() + .flatMapObservable { walletAddressModel -> + Observable.interval(0, 10, TimeUnit.SECONDS, Schedulers.io()) + .timeInterval() + .switchMap { + adyenPaymentRepository.getTransaction(uid, walletAddressModel.address, + walletAddressModel.signedAddress) + .toObservable() + } + .filter { isEndingState(it.status) } + .distinctUntilChanged { transaction -> transaction.status } + } + } + + fun getFailedTransactionReason(uid: String, timesCalled: Int = 0): Single { + return if (timesCalled < MAX_NUMBER_OF_TRIES) { + walletService.getAndSignCurrentWalletAddress() + .flatMap { walletAddressModel -> + Single.zip(adyenPaymentRepository.getTransaction(uid, walletAddressModel.address, + walletAddressModel.signedAddress), + Single.timer(REQUEST_INTERVAL_IN_SECONDS, TimeUnit.SECONDS), + BiFunction { paymentModel: PaymentModel, _: Long -> paymentModel }) + } + .flatMap { + if (it.errorCode != null) Single.just(it) + else getFailedTransactionReason(it.uid, timesCalled + 1) + } + } else { + Single.just(PaymentModel(Error(true))) + } + } + + fun getWalletAddress() = walletService.getWalletAddress() + + private fun isEndingState(status: TransactionResponse.Status): Boolean { + return (status == TransactionResponse.Status.COMPLETED + || status == TransactionResponse.Status.FAILED + || status == TransactionResponse.Status.CANCELED + || status == TransactionResponse.Status.INVALID_TRANSACTION + || status == TransactionResponse.Status.FRAUD) + } + + private fun isInApp(type: String): Boolean { + return type.equals("INAPP", ignoreCase = true) + } + + private companion object { + private const val MAX_NUMBER_OF_TRIES = 5 + private const val REQUEST_INTERVAL_IN_SECONDS: Long = 2 + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenPaymentPresenter.kt b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenPaymentPresenter.kt new file mode 100644 index 00000000000..210675c73e2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenPaymentPresenter.kt @@ -0,0 +1,648 @@ +package com.asfoundation.wallet.billing.adyen + +import android.os.Bundle +import androidx.annotation.StringRes +import com.adyen.checkout.base.model.paymentmethods.PaymentMethod +import com.appcoins.wallet.billing.adyen.AdyenBillingAddress +import com.appcoins.wallet.billing.adyen.AdyenPaymentRepository +import com.appcoins.wallet.billing.adyen.AdyenResponseMapper.Companion.REDIRECT +import com.appcoins.wallet.billing.adyen.AdyenResponseMapper.Companion.THREEDS2CHALLENGE +import com.appcoins.wallet.billing.adyen.AdyenResponseMapper.Companion.THREEDS2FINGERPRINT +import com.appcoins.wallet.billing.adyen.PaymentModel +import com.appcoins.wallet.billing.adyen.TransactionResponse.Status +import com.appcoins.wallet.billing.adyen.TransactionResponse.Status.* +import com.appcoins.wallet.billing.util.Error +import com.asfoundation.wallet.analytics.FacebookEventLogger +import com.asfoundation.wallet.billing.address.BillingAddressModel +import com.asfoundation.wallet.billing.adyen.AdyenErrorCodeMapper.Companion.CVC_DECLINED +import com.asfoundation.wallet.billing.adyen.AdyenErrorCodeMapper.Companion.FRAUD +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.service.ServicesErrorCodeMapper +import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor +import com.asfoundation.wallet.ui.iab.Navigator +import com.asfoundation.wallet.ui.iab.PaymentMethodsView +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import java.math.BigDecimal +import java.util.concurrent.TimeUnit + +class AdyenPaymentPresenter(private val view: AdyenPaymentView, + private val disposables: CompositeDisposable, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val returnUrl: String, + private val analytics: BillingAnalytics, + private val domain: String, + private val origin: String?, + private val adyenPaymentInteractor: AdyenPaymentInteractor, + private val transactionBuilder: Single, + private val navigator: Navigator, + private val paymentType: String, + private val transactionType: String, + private val amount: BigDecimal, + private val currency: String, + private val isPreSelected: Boolean, + private val adyenErrorCodeMapper: AdyenErrorCodeMapper, + private val servicesErrorCodeMapper: ServicesErrorCodeMapper, + private val gamificationLevel: Int, + private val formatter: CurrencyFormatUtils, + private val logger: Logger) { + + private var waitingResult = false + private var cachedUid = "" + private var cachedPaymentData: String? = null + + fun present(savedInstanceState: Bundle?) { + retrieveSavedInstace(savedInstanceState) + view.setup3DSComponent() + view.setupRedirectComponent() + if (!waitingResult) loadPaymentMethodInfo(savedInstanceState) + handleBack() + handleErrorDismissEvent() + handleForgetCardClick() + handleRedirectResponse() + handlePaymentDetails() + handleAdyenErrorBack() + handleAdyenErrorCancel() + handleSupportClicks() + handle3DSErrors() + if (isPreSelected) handleMorePaymentsClick() + } + + private fun handleSupportClicks() { + disposables.add( + Observable.merge(view.getAdyenSupportIconClicks(), view.getAdyenSupportLogoClicks()) + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .flatMapCompletable { adyenPaymentInteractor.showSupport(gamificationLevel) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleForgetCardClick() { + disposables.add(view.forgetCardClick() + .observeOn(viewScheduler) + .doOnNext { view.showLoading() } + .observeOn(networkScheduler) + .flatMapSingle { adyenPaymentInteractor.disablePayments() } + .observeOn(viewScheduler) + .doOnNext { success -> if (!success) view.showGenericError() } + .filter { it } + .observeOn(networkScheduler) + .flatMapSingle { + adyenPaymentInteractor.loadPaymentInfo(mapPaymentToService(paymentType), + amount.toString(), currency) + .observeOn(viewScheduler) + .doOnSuccess { + view.hideLoadingAndShowView() + if (it.error.hasError) { + if (it.error.isNetworkError) view.showNetworkError() + else view.showGenericError() + } else { + view.finishCardConfiguration(it.paymentMethodInfo!!, it.isStored, true, null) + } + } + } + .subscribe({}, { + logger.log(TAG, it) + view.showGenericError() + })) + } + + private fun loadPaymentMethodInfo(savedInstanceState: Bundle?) { + view.showLoading() + disposables.add( + adyenPaymentInteractor.loadPaymentInfo(mapPaymentToService(paymentType), amount.toString(), + currency) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { + if (it.error.hasError) { + sendPaymentErrorEvent(it.error.code, it.error.message) + view.hideLoadingAndShowView() + handleErrors(it.error) + } else { + val amount = formatter.formatCurrency(it.priceAmount, WalletCurrency.FIAT) + view.showProductPrice(amount, it.priceCurrency) + if (paymentType == PaymentType.CARD.name) { + view.hideLoadingAndShowView() + sendPaymentMethodDetailsEvent(BillingAnalytics.PAYMENT_METHOD_CC) + view.finishCardConfiguration(it.paymentMethodInfo!!, it.isStored, false, + savedInstanceState) + handleBuyClick(it.priceAmount, it.priceCurrency) + } else if (paymentType == PaymentType.PAYPAL.name) { + launchPaypal(it.paymentMethodInfo!!, it.priceAmount, it.priceCurrency) + } + } + } + .subscribe({}, { + logger.log(TAG, it) + view.showGenericError() + })) + } + + private fun launchPaypal(paymentMethodInfo: PaymentMethod, priceAmount: BigDecimal, + priceCurrency: String) { + disposables.add(transactionBuilder.flatMap { + adyenPaymentInteractor.makePayment(paymentMethodInfo, false, false, emptyList(), returnUrl, + priceAmount.toString(), priceCurrency, it.orderReference, + mapPaymentToService(paymentType).transactionType, origin, domain, it.payload, + it.skuId, it.callbackUrl, it.type, it.toAddress()) + } + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .filter { !waitingResult } + .doOnSuccess { + view.hideLoadingAndShowView() + handlePaymentModel(it) + } + .subscribe({}, { + logger.log(TAG, it) + view.showGenericError() + })) + } + + private fun handlePaymentModel(paymentModel: PaymentModel) { + if (paymentModel.error.hasError) { + handleErrors(paymentModel.error) + } else { + view.showLoading() + view.lockRotation() + sendPaymentMethodDetailsEvent(mapPaymentToAnalytics(paymentType)) + handleAdyenAction(paymentModel) + } + } + + private fun handleBuyClick(priceAmount: BigDecimal, priceCurrency: String) { + disposables.add(Observable.merge(view.buyButtonClicked(), view.billingAddressInput()) + .flatMapSingle { + view.retrievePaymentData() + .firstOrError() + } + .observeOn(viewScheduler) + .doOnNext { + view.showLoading() + view.hideKeyboard() + view.lockRotation() + } + .observeOn(networkScheduler) + .flatMapSingle { adyenCard -> + transactionBuilder + .flatMap { + handleBuyAnalytics(it) + val billingAddressModel = view.retrieveBillingAddressData() + val shouldStore = billingAddressModel?.remember ?: adyenCard.shouldStoreCard + adyenPaymentInteractor.makePayment(adyenCard.cardPaymentMethod, + shouldStore, adyenCard.hasCvc, adyenCard.supportedShopperInteractions, + returnUrl, priceAmount.toString(), priceCurrency, it.orderReference, + mapPaymentToService(paymentType).transactionType, origin, domain, + it.payload, it.skuId, it.callbackUrl, it.type, it.toAddress(), + mapToAdyenBillingAddress(billingAddressModel)) + } + } + .observeOn(viewScheduler) + .flatMapCompletable { handlePaymentResult(it, priceAmount, priceCurrency) } + .subscribe({}, { + logger.log(TAG, it) + view.showGenericError() + })) + } + + private fun handlePaymentResult(paymentModel: PaymentModel, priceAmount: BigDecimal? = null, + priceCurrency: String? = null): Completable { + return when { + paymentModel.resultCode.equals("AUTHORISED", true) -> { + adyenPaymentInteractor.getAuthorisedTransaction(paymentModel.uid) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .flatMapCompletable { + when { + it.status == COMPLETED -> { + sendPaymentSuccessEvent() + createBundle(it.hash, it.orderReference) + .doOnSuccess { + sendPaymentEvent() + sendRevenueEvent() + } + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .flatMapCompletable { bundle -> handleSuccessTransaction(bundle) } + } + isPaymentFailed(it.status) -> { + if (paymentModel.status == FAILED && paymentType == PaymentType.PAYPAL.name) { + retrieveFailedReason(paymentModel.uid) + } else { + Completable.fromAction { + sendPaymentErrorEvent(it.error.code, + buildRefusalReason(it.status, it.error.message)) + handleErrors(it.error) + } + .subscribeOn(viewScheduler) + } + } + else -> { + sendPaymentErrorEvent(it.error.code, it.status.toString()) + Completable.fromAction { handleErrors(it.error) } + } + } + } + } + paymentModel.status == PENDING_USER_PAYMENT && paymentModel.action != null -> { + Completable.fromAction { + view.showLoading() + view.lockRotation() + handleAdyenAction(paymentModel) + } + } + paymentModel.refusalReason != null -> Completable.fromAction { + sendPaymentErrorEvent(paymentModel.refusalCode, paymentModel.refusalReason) + paymentModel.refusalCode?.let { code -> + when (code) { + CVC_DECLINED -> view.showCvvError() + FRAUD -> handleFraudFlow(adyenErrorCodeMapper.map(code)) + else -> view.showSpecificError(adyenErrorCodeMapper.map(code)) + } + } + } + paymentModel.error.hasError -> Completable.fromAction { + if (isBillingAddressError(paymentModel.error, priceAmount, priceCurrency)) { + view.showBillingAddress(priceAmount!!, priceCurrency!!) + } else { + sendPaymentErrorEvent(paymentModel.error.code, paymentModel.error.message) + handleErrors(paymentModel.error) + } + } + paymentModel.status == FAILED && paymentType == PaymentType.PAYPAL.name -> { + retrieveFailedReason(paymentModel.uid) + } + paymentModel.status == CANCELED -> Completable.fromAction { view.showMoreMethods() } + else -> Completable.fromAction { + sendPaymentErrorEvent(paymentModel.error.code, "${paymentModel.status}: Generic Error") + view.showGenericError() + } + } + } + + private fun isBillingAddressError(error: Error, + priceAmount: BigDecimal?, + priceCurrency: String?): Boolean { + return error.code != null + && error.code == 400 + && error.message?.contains("payment.billing_address") == true + && priceAmount != null + && priceCurrency != null + } + + private fun handleSuccessTransaction(bundle: Bundle): Completable { + return Completable.fromAction { view.showSuccess() } + .andThen(Completable.timer(view.getAnimationDuration(), + TimeUnit.MILLISECONDS)) + .andThen(Completable.fromAction { navigator.popView(bundle) }) + } + + private fun retrieveFailedReason(uid: String): Completable { + return adyenPaymentInteractor.getFailedTransactionReason(uid) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .flatMapCompletable { + Completable.fromAction { + sendPaymentErrorEvent(it.errorCode, it.errorMessage ?: "") + if (it.errorCode != null) view.showSpecificError( + adyenErrorCodeMapper.map(it.errorCode!!)) + else view.showGenericError() + } + } + } + + private fun handleFraudFlow(@StringRes error: Int) { + disposables.add( + adyenPaymentInteractor.isWalletBlocked() + .subscribeOn(networkScheduler) + .observeOn(networkScheduler) + .flatMap { blocked -> + if (blocked) { + adyenPaymentInteractor.isWalletVerified() + .observeOn(viewScheduler) + .doOnSuccess { + if (it) view.showSpecificError(error) + else view.showWalletValidation(error) + } + } else { + Single.just(true) + .observeOn(viewScheduler) + .doOnSuccess { view.showSpecificError(error) } + } + } + .observeOn(viewScheduler) + .subscribe({}, { + view.showSpecificError(error) + logger.log(TAG, it) + }) + ) + } + + private fun buildRefusalReason(status: Status, message: String?): String { + return message?.let { "$status : $it" } ?: status.toString() + } + + private fun isPaymentFailed(status: Status): Boolean { + return status == FAILED || status == CANCELED || status == INVALID_TRANSACTION + } + + private fun handlePaymentDetails() { + disposables.add(view.getPaymentDetails() + .throttleLast(2, TimeUnit.SECONDS) + .observeOn(viewScheduler) + .doOnNext { view.lockRotation() } + .observeOn(networkScheduler) + .flatMapSingle { + adyenPaymentInteractor.submitRedirect(cachedUid, it.details!!, + it.paymentData ?: cachedPaymentData) + } + .observeOn(viewScheduler) + .flatMapCompletable { handlePaymentResult(it) } + .subscribe({}, { + logger.log(TAG, it) + view.showGenericError() + })) + } + + private fun handle3DSErrors() { + disposables.add(view.onAdyen3DSError() + .observeOn(viewScheduler) + .doOnNext { + if (it == CHALLENGE_CANCELED) view.showMoreMethods() + else { + logger.log(TAG, it) + view.showGenericError() + } + } + .subscribe({}, { it.printStackTrace() })) + } + + fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(WAITING_RESULT, waitingResult) + outState.putString(UID, cachedUid) + outState.putString(PAYMENT_DATA, cachedPaymentData) + } + + private fun retrieveSavedInstace(savedInstanceState: Bundle?) { + savedInstanceState?.let { + waitingResult = it.getBoolean(WAITING_RESULT) + cachedUid = it.getString(UID, "") + cachedPaymentData = it.getString(PAYMENT_DATA) + } + } + + private fun sendPaymentMethodDetailsEvent(paymentMethod: String) { + disposables.add(transactionBuilder.subscribe { transactionBuilder: TransactionBuilder -> + analytics.sendPaymentMethodDetailsEvent(domain, transactionBuilder.skuId, + transactionBuilder.amount() + .toString(), paymentMethod, transactionBuilder.type) + }) + } + + private fun handleErrorDismissEvent() { + disposables.add(view.errorDismisses() + .observeOn(viewScheduler) + .doOnNext { navigator.popViewWithError() } + .subscribe({}, { navigator.popViewWithError() })) + } + + private fun handleBack() { + disposables.add(view.backEvent() + .observeOn(networkScheduler) + .flatMapSingle { transactionBuilder } + .doOnNext { handlePaymentMethodAnalytics(it) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handlePaymentMethodAnalytics(transaction: TransactionBuilder) { + if (isPreSelected) { + analytics.sendPreSelectedPaymentMethodEvent(domain, transaction.skuId, + transaction.amount() + .toString(), mapPaymentToService(paymentType).transactionType, + transaction.type, "cancel") + view.close(adyenPaymentInteractor.mapCancellation()) + } else { + analytics.sendPaymentConfirmationEvent(domain, transaction.skuId, + transaction.amount() + .toString(), mapPaymentToService(paymentType).transactionType, + transaction.type, "back") + view.showMoreMethods() + } + } + + private fun handleMorePaymentsClick() { + disposables.add(view.getMorePaymentMethodsClicks() + .observeOn(networkScheduler) + .flatMapSingle { transactionBuilder } + .doOnNext { handleMorePaymentsAnalytics(it) } + .observeOn(viewScheduler) + .doOnNext { showMoreMethods() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleMorePaymentsAnalytics(transaction: TransactionBuilder) { + analytics.sendPreSelectedPaymentMethodEvent(domain, transaction.skuId, + transaction.amount() + .toString(), mapPaymentToService(paymentType).transactionType, + transaction.type, "other_payments") + } + + private fun handleRedirectResponse() { + disposables.add(navigator.uriResults() + .observeOn(viewScheduler) + .doOnNext { view.submitUriResult(it) } + .subscribe({}, { + logger.log(TAG, it) + view.showGenericError() + })) + } + + private fun showMoreMethods() { + adyenPaymentInteractor.removePreSelectedPaymentMethod() + view.showMoreMethods() + } + + private fun sendPaymentEvent() { + disposables.add(transactionBuilder.subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .subscribe { transactionBuilder: TransactionBuilder -> + analytics.sendPaymentEvent(domain, transactionBuilder.skuId, + transactionBuilder.amount() + .toString(), mapPaymentToAnalytics(paymentType), + transactionBuilder.type) + }) + } + + private fun sendRevenueEvent() { + disposables.add(transactionBuilder.subscribe { transactionBuilder: TransactionBuilder -> + analytics.sendRevenueEvent(adyenPaymentInteractor.convertToFiat(transactionBuilder.amount() + .toDouble(), FacebookEventLogger.EVENT_REVENUE_CURRENCY) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .blockingGet() + .amount + .setScale(2, BigDecimal.ROUND_UP) + .toString()) + }) + } + + private fun sendPaymentSuccessEvent() { + disposables.add(transactionBuilder + .observeOn(networkScheduler) + .doOnSuccess { transaction -> + analytics.sendPaymentSuccessEvent(domain, transaction.skuId, + transaction.amount() + .toString(), + mapPaymentToAnalytics(paymentType), transaction.type) + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun sendPaymentErrorEvent(refusalCode: Int?, refusalReason: String?) { + disposables.add(transactionBuilder + .observeOn(networkScheduler) + .doOnSuccess { transaction -> + analytics.sendPaymentErrorWithDetailsEvent(domain, transaction.skuId, + transaction.amount() + .toString(), mapPaymentToAnalytics(paymentType), transaction.type, + refusalCode.toString(), refusalReason) + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun mapPaymentToAnalytics(paymentType: String): String { + return if (paymentType == PaymentType.CARD.name) { + BillingAnalytics.PAYMENT_METHOD_CC + } else { + BillingAnalytics.PAYMENT_METHOD_PAYPAL + } + } + + private fun mapPaymentToService(paymentType: String): AdyenPaymentRepository.Methods { + return if (paymentType == PaymentType.CARD.name) { + AdyenPaymentRepository.Methods.CREDIT_CARD + } else { + AdyenPaymentRepository.Methods.PAYPAL + } + } + + private fun mapToAdyenBillingAddress( + billingAddressModel: BillingAddressModel?): AdyenBillingAddress? { + return billingAddressModel?.let { + AdyenBillingAddress(it.address, it.city, it.zipcode, it.number, it.state, it.country) + } + } + + private fun createBundle(hash: String?, orderReference: String?): Single { + return transactionBuilder.flatMap { + adyenPaymentInteractor.getCompletePurchaseBundle(transactionType, domain, it.skuId, + orderReference, hash, networkScheduler) + } + .map { mapPaymentMethodId(it) } + } + + private fun mapPaymentMethodId(bundle: Bundle): Bundle { + if (paymentType == PaymentType.CARD.name) { + bundle.putString(InAppPurchaseInteractor.PRE_SELECTED_PAYMENT_METHOD_KEY, + PaymentMethodsView.PaymentMethodId.CREDIT_CARD.id) + } else if (paymentType == PaymentType.PAYPAL.name) { + bundle.putString(InAppPurchaseInteractor.PRE_SELECTED_PAYMENT_METHOD_KEY, + PaymentMethodsView.PaymentMethodId.PAYPAL.id) + } + return bundle + } + + private fun handleBuyAnalytics(transactionBuilder: TransactionBuilder) { + if (isPreSelected) { + analytics.sendPreSelectedPaymentMethodEvent(domain, transactionBuilder.skuId, + transactionBuilder.amount() + .toString(), mapPaymentToService(paymentType).transactionType, + transactionBuilder.type, "buy") + } else { + analytics.sendPaymentConfirmationEvent(domain, transactionBuilder.skuId, + transactionBuilder.amount() + .toString(), mapPaymentToService(paymentType).transactionType, + transactionBuilder.type, "buy") + } + } + + private fun handleAdyenErrorBack() { + disposables.add(view.adyenErrorBackClicks() + .observeOn(viewScheduler) + .doOnNext { + if (isPreSelected) { + view.close(adyenPaymentInteractor.mapCancellation()) + } else { + view.showMoreMethods() + } + } + .subscribe({}, { + logger.log(TAG, it) + view.showGenericError() + } + )) + } + + private fun handleAdyenErrorCancel() { + disposables.add(view.adyenErrorCancelClicks() + .observeOn(viewScheduler) + .doOnNext { view.close(adyenPaymentInteractor.mapCancellation()) } + .subscribe({}, { + logger.log(TAG, it) + view.showGenericError() + })) + } + + private fun handleAdyenAction(paymentModel: PaymentModel) { + if (paymentModel.action != null) { + val type = paymentModel.action?.type + if (type == REDIRECT) { + cachedPaymentData = paymentModel.paymentData + cachedUid = paymentModel.uid + navigator.navigateToUriForResult(paymentModel.redirectUrl) + waitingResult = true + } else if (type == THREEDS2FINGERPRINT || type == THREEDS2CHALLENGE) { + cachedUid = paymentModel.uid + view.handle3DSAction(paymentModel.action!!) + waitingResult = true + } else { + logger.log(TAG, "Unknown adyen action: $type") + view.showGenericError() + } + } + } + + fun stop() = disposables.clear() + + companion object { + + private const val WAITING_RESULT = "WAITING_RESULT" + private const val HTTP_FRAUD_CODE = 403 + private const val UID = "UID" + private const val PAYMENT_DATA = "payment_data" + private const val CHALLENGE_CANCELED = "Challenge canceled." + private val TAG = AdyenPaymentPresenter::class.java.name + } + + private fun handleErrors(error: Error) { + when { + error.isNetworkError -> view.showNetworkError() + error.code != null -> { + val resId = servicesErrorCodeMapper.mapError(error.code!!) + if (error.code == HTTP_FRAUD_CODE) handleFraudFlow(resId) + else view.showSpecificError(resId) + } + else -> view.showGenericError() + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenPaymentView.kt b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenPaymentView.kt new file mode 100644 index 00000000000..7b2fd99e456 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/adyen/AdyenPaymentView.kt @@ -0,0 +1,85 @@ +package com.asfoundation.wallet.billing.adyen + +import android.net.Uri +import android.os.Bundle +import androidx.annotation.StringRes +import com.adyen.checkout.base.model.payments.response.Action +import com.asfoundation.wallet.billing.address.BillingAddressModel +import io.reactivex.Observable +import io.reactivex.subjects.ReplaySubject +import java.math.BigDecimal + +interface AdyenPaymentView { + + fun getAnimationDuration(): Long + + fun showProduct() + + fun showLoading() + + fun errorDismisses(): Observable + + fun buyButtonClicked(): Observable + + fun showNetworkError() + + fun backEvent(): Observable + + fun close(bundle: Bundle?) + + fun showSuccess() + + fun showGenericError() + + fun getMorePaymentMethodsClicks(): Observable + + fun showMoreMethods() + + fun hideLoadingAndShowView() + + fun finishCardConfiguration( + paymentMethod: com.adyen.checkout.base.model.paymentmethods.PaymentMethod, isStored: Boolean, + forget: Boolean, savedInstance: Bundle?) + + fun retrievePaymentData(): ReplaySubject + + fun retrieveBillingAddressData(): BillingAddressModel? + + fun billingAddressInput(): Observable + + fun showSpecificError(stringRes: Int) + + fun showCvvError() + + fun showProductPrice(amount: String, currencyCode: String) + + fun lockRotation() + + fun setupRedirectComponent() + + fun submitUriResult(uri: Uri) + + fun getPaymentDetails(): Observable + + fun forgetCardClick(): Observable + + fun hideKeyboard() + + fun adyenErrorCancelClicks(): Observable + + fun adyenErrorBackClicks(): Observable + + fun getAdyenSupportLogoClicks(): Observable + + fun getAdyenSupportIconClicks(): Observable + + fun showWalletValidation(@StringRes error: Int) + + fun handle3DSAction(action: Action) + + fun onAdyen3DSError(): Observable + + fun setup3DSComponent() + + fun showBillingAddress(value: BigDecimal, currency: String) +} diff --git a/app/src/main/java/com/asfoundation/wallet/billing/adyen/PaymentType.java b/app/src/main/java/com/asfoundation/wallet/billing/adyen/PaymentType.java new file mode 100644 index 00000000000..125e549cac9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/adyen/PaymentType.java @@ -0,0 +1,21 @@ +package com.asfoundation.wallet.billing.adyen; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public enum PaymentType { + CARD(Arrays.asList("visa", "mastercard", "card", "credit_card")), PAYPAL( + Collections.singletonList("paypal")), LOCAL_PAYMENTS( + Collections.singletonList("localPayments")); + + private final List subTypes; + + PaymentType(List subTypes) { + this.subTypes = subTypes; + } + + public List getSubTypes() { + return subTypes; + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/billing/analytics/BillingAnalytics.java b/app/src/main/java/com/asfoundation/wallet/billing/analytics/BillingAnalytics.java new file mode 100644 index 00000000000..f746544bb13 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/analytics/BillingAnalytics.java @@ -0,0 +1,250 @@ +package com.asfoundation.wallet.billing.analytics; + +import cm.aptoide.analytics.AnalyticsManager; +import java.util.HashMap; +import java.util.Map; + +public class BillingAnalytics implements EventSender { + public static final String PURCHASE_DETAILS = "PURCHASE_DETAILS"; + public static final String PAYMENT_METHOD_DETAILS = "PAYMENT_METHOD_DETAILS"; + public static final String PAYMENT = "PAYMENT"; + public static final String REVENUE = "REVENUE"; + public static final String PAYMENT_METHOD_APPC = "APPC"; + public static final String PAYMENT_METHOD_CC = "CREDIT_CARD"; + public static final String PAYMENT_METHOD_REWARDS = "REWARDS"; + public static final String PAYMENT_METHOD_PAYPAL = "PAYPAL"; + public static final String RAKAM_PRESELECTED_PAYMENT_METHOD = "wallet_preselected_payment_method"; + public static final String RAKAM_PAYMENT_METHOD = "wallet_payment_method"; + public static final String RAKAM_PAYMENT_CONFIRMATION = "wallet_payment_confirmation"; + public static final String RAKAM_PAYMENT_CONCLUSION = "wallet_payment_conclusion"; + public static final String RAKAM_PAYMENT_START = "wallet_payment_start"; + public static final String RAKAM_PAYPAL_URL = "wallet_payment_conclusion_paypal"; + private static final String WALLET = "WALLET"; + private static final String EVENT_PACKAGE_NAME = "package_name"; + private static final String EVENT_SKU = "sku"; + private static final String EVENT_VALUE = "value"; + private static final String EVENT_PURCHASE = "purchase"; + private static final String EVENT_TRANSACTION_TYPE = "transaction_type"; + private static final String EVENT_PAYMENT_METHOD = "payment_method"; + private static final String EVENT_ACTION = "action"; + private static final String EVENT_CONTEXT = "context"; + private static final String EVENT_STATUS = "status"; + private static final String EVENT_ERROR_CODE = "error_code"; + private static final String EVENT_ERROR_DETAILS = "error_details"; + private static final String EVENT_SUCCESS = "success"; + private static final String EVENT_FAIL = "fail"; + private static final String EVENT_PENDING = "pending"; + private static final String EVENT_PAYPAL_TYPE = "type"; + private static final String EVENT_RESULT_CODE = "result_code"; + private static final String EVENT_URL = "url"; + private static final int MAX_CHARACTERS = 100; + private final AnalyticsManager analytics; + + public BillingAnalytics(AnalyticsManager analytics) { + this.analytics = analytics; + } + + @Override + public void sendPurchaseDetailsEvent(String packageName, String skuDetails, String value, + String transactionType) { + Map eventData = new HashMap<>(); + Map purchaseData = new HashMap<>(); + + purchaseData.put(EVENT_PACKAGE_NAME, packageName); + purchaseData.put(EVENT_SKU, skuDetails); + purchaseData.put(EVENT_VALUE, value); + + eventData.put(EVENT_PURCHASE, purchaseData); + eventData.put(EVENT_TRANSACTION_TYPE, transactionType); + + analytics.logEvent(eventData, PURCHASE_DETAILS, AnalyticsManager.Action.CLICK, WALLET); + } + + @Override + public void sendPaymentMethodDetailsEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType) { + Map eventData = new HashMap<>(); + Map purchaseData = new HashMap<>(); + + purchaseData.put(EVENT_PACKAGE_NAME, packageName); + purchaseData.put(EVENT_SKU, skuDetails); + purchaseData.put(EVENT_VALUE, value); + + eventData.put(EVENT_PURCHASE, purchaseData); + eventData.put(EVENT_PAYMENT_METHOD, purchaseDetails); + eventData.put(EVENT_TRANSACTION_TYPE, transactionType); + + analytics.logEvent(eventData, PAYMENT_METHOD_DETAILS, AnalyticsManager.Action.CLICK, WALLET); + } + + @Override public void sendPaymentEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType) { + Map eventData = new HashMap<>(); + Map purchaseData = new HashMap<>(); + + purchaseData.put(EVENT_PACKAGE_NAME, packageName); + purchaseData.put(EVENT_SKU, skuDetails); + purchaseData.put(EVENT_VALUE, value); + + eventData.put(EVENT_PURCHASE, purchaseData); + eventData.put(EVENT_PAYMENT_METHOD, purchaseDetails); + eventData.put(EVENT_TRANSACTION_TYPE, transactionType); + + analytics.logEvent(eventData, PAYMENT, AnalyticsManager.Action.IMPRESSION, WALLET); + } + + @Override public void sendRevenueEvent(String value) { + Map eventData = new HashMap<>(); + + eventData.put(EVENT_VALUE, value); + + analytics.logEvent(eventData, REVENUE, AnalyticsManager.Action.IMPRESSION, WALLET); + } + + @Override + public void sendPreSelectedPaymentMethodEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType, String action) { + Map eventData = + createBaseRakamEventMap(packageName, skuDetails, value, purchaseDetails, transactionType, + action); + + analytics.logEvent(eventData, RAKAM_PRESELECTED_PAYMENT_METHOD, AnalyticsManager.Action.CLICK, + WALLET); + } + + @Override public void sendPaymentMethodEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType, String action) { + Map eventData = + createBaseRakamEventMap(packageName, skuDetails, value, purchaseDetails, transactionType, + action); + + analytics.logEvent(eventData, RAKAM_PAYMENT_METHOD, AnalyticsManager.Action.CLICK, WALLET); + } + + @Override + public void sendPaymentConfirmationEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType, String action) { + Map eventData = + createBaseRakamEventMap(packageName, skuDetails, value, purchaseDetails, transactionType, + action); + + analytics.logEvent(eventData, RAKAM_PAYMENT_CONFIRMATION, AnalyticsManager.Action.CLICK, + WALLET); + } + + @Override public void sendPaymentErrorEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType, String errorCode) { + Map eventData = + createConclusionRakamEventMap(packageName, skuDetails, value, purchaseDetails, + transactionType, EVENT_FAIL); + + eventData.put(EVENT_ERROR_CODE, errorCode); + + analytics.logEvent(eventData, RAKAM_PAYMENT_CONCLUSION, AnalyticsManager.Action.CLICK, WALLET); + } + + @Override + public void sendPaymentErrorWithDetailsEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType, String errorCode, String errorDetails) { + Map eventData = + createConclusionRakamEventMap(packageName, skuDetails, value, purchaseDetails, + transactionType, EVENT_FAIL); + + eventData.put(EVENT_ERROR_CODE, errorCode); + eventData.put(EVENT_ERROR_DETAILS, errorDetails); + + analytics.logEvent(eventData, RAKAM_PAYMENT_CONCLUSION, AnalyticsManager.Action.CLICK, WALLET); + } + + @Override public void sendPaymentSuccessEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType) { + Map eventData = + createConclusionRakamEventMap(packageName, skuDetails, value, purchaseDetails, + transactionType, EVENT_SUCCESS); + + analytics.logEvent(eventData, RAKAM_PAYMENT_CONCLUSION, AnalyticsManager.Action.CLICK, WALLET); + } + + @Override public void sendPaymentPendingEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType) { + Map eventData = + createConclusionRakamEventMap(packageName, skuDetails, value, purchaseDetails, + transactionType, EVENT_PENDING); + + analytics.logEvent(eventData, RAKAM_PAYMENT_CONCLUSION, AnalyticsManager.Action.CLICK, WALLET); + } + + @Override public void sendPurchaseStartEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType, String context) { + Map eventData = new HashMap<>(); + + eventData.put(EVENT_PACKAGE_NAME, packageName); + eventData.put(EVENT_SKU, skuDetails); + eventData.put(EVENT_VALUE, value); + eventData.put(EVENT_TRANSACTION_TYPE, transactionType); + eventData.put(EVENT_PAYMENT_METHOD, purchaseDetails); + eventData.put(EVENT_CONTEXT, context); + + analytics.logEvent(eventData, RAKAM_PAYMENT_START, AnalyticsManager.Action.CLICK, WALLET); + } + + @Override public void sendPurchaseStartWithoutDetailsEvent(String packageName, String skuDetails, + String value, String transactionType, String context) { + Map eventData = new HashMap<>(); + + eventData.put(EVENT_PACKAGE_NAME, packageName); + eventData.put(EVENT_SKU, skuDetails); + eventData.put(EVENT_VALUE, value); + eventData.put(EVENT_TRANSACTION_TYPE, transactionType); + eventData.put(EVENT_CONTEXT, context); + + analytics.logEvent(eventData, RAKAM_PAYMENT_START, AnalyticsManager.Action.CLICK, WALLET); + } + + @Override public void sendPaypalUrlEvent(String packageName, String skuDetails, String value, + String transactionType, String type, String resultCode, String url) { + Map eventData = new HashMap<>(); + + eventData.put(EVENT_PACKAGE_NAME, packageName); + eventData.put(EVENT_SKU, skuDetails); + eventData.put(EVENT_VALUE, value); + eventData.put(EVENT_TRANSACTION_TYPE, transactionType); + eventData.put(EVENT_PAYPAL_TYPE, type); + eventData.put(EVENT_RESULT_CODE, resultCode); + if (url.length() > MAX_CHARACTERS) { + eventData.put(EVENT_URL, url.substring(url.length() - MAX_CHARACTERS)); + } else { + eventData.put(EVENT_URL, url); + } + + analytics.logEvent(eventData, RAKAM_PAYPAL_URL, AnalyticsManager.Action.CLICK, WALLET); + } + + private Map createBaseRakamEventMap(String packageName, String skuDetails, + String value, String purchaseDetails, String transactionType, String action) { + Map eventData = new HashMap<>(); + + eventData.put(EVENT_PACKAGE_NAME, packageName); + eventData.put(EVENT_SKU, skuDetails); + eventData.put(EVENT_VALUE, value); + eventData.put(EVENT_TRANSACTION_TYPE, transactionType); + eventData.put(EVENT_PAYMENT_METHOD, purchaseDetails); + eventData.put(EVENT_ACTION, action); + + return eventData; + } + + private Map createConclusionRakamEventMap(String packageName, String skuDetails, + String value, String purchaseDetails, String transactionType, String status) { + Map eventData = new HashMap<>(); + + eventData.put(EVENT_PACKAGE_NAME, packageName); + eventData.put(EVENT_SKU, skuDetails); + eventData.put(EVENT_VALUE, value); + eventData.put(EVENT_TRANSACTION_TYPE, transactionType); + eventData.put(EVENT_PAYMENT_METHOD, purchaseDetails); + eventData.put(EVENT_STATUS, status); + + return eventData; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/analytics/EventSender.java b/app/src/main/java/com/asfoundation/wallet/billing/analytics/EventSender.java new file mode 100644 index 00000000000..90813797d8e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/analytics/EventSender.java @@ -0,0 +1,45 @@ +package com.asfoundation.wallet.billing.analytics; + +public interface EventSender { + + void sendPurchaseDetailsEvent(String packageName, String skuDetails, String value, + String transactionType); + + void sendPaymentMethodDetailsEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType); + + void sendPaymentEvent(String packageName, String skuDetails, String value, String purchaseDetails, + String transactionType); + + void sendRevenueEvent(String value); + + void sendPreSelectedPaymentMethodEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType, String action); + + void sendPaymentMethodEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType, String action); + + void sendPaymentConfirmationEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType, String action); + + void sendPaymentErrorEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType, String errorCode); + + void sendPaymentErrorWithDetailsEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType, String errorCode, String errorDetails); + + void sendPaymentSuccessEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType); + + void sendPaymentPendingEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType); + + void sendPurchaseStartEvent(String packageName, String skuDetails, String value, + String purchaseDetails, String transactionType, String context); + + void sendPurchaseStartWithoutDetailsEvent(String packageName, String skuDetails, String value, + String transactionType, String context); + + void sendPaypalUrlEvent(String packageName, String skuDetails, String value, + String transactionType, String type, String resultCode, String url); +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/analytics/PageViewAnalytics.kt b/app/src/main/java/com/asfoundation/wallet/billing/analytics/PageViewAnalytics.kt new file mode 100644 index 00000000000..73f9b26f942 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/analytics/PageViewAnalytics.kt @@ -0,0 +1,24 @@ +package com.asfoundation.wallet.billing.analytics + +import cm.aptoide.analytics.AnalyticsManager +import java.util.* + +class PageViewAnalytics(private val analyticsManager: AnalyticsManager) { + + fun sendPageViewEvent(context: String) { + val eventData = HashMap() + + eventData[CONTEXT] = context + + analyticsManager.logEvent(eventData, WALLET_PAGE_VIEW, + AnalyticsManager.Action.CLICK, WALLET) + } + + companion object { + const val WALLET_PAGE_VIEW = "wallet_page_view" + + private const val CONTEXT = "context" + + private const val WALLET = "wallet" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/analytics/PoaAnalytics.java b/app/src/main/java/com/asfoundation/wallet/billing/analytics/PoaAnalytics.java new file mode 100644 index 00000000000..bef3e143a83 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/analytics/PoaAnalytics.java @@ -0,0 +1,55 @@ +package com.asfoundation.wallet.billing.analytics; + +import android.text.TextUtils; +import cm.aptoide.analytics.AnalyticsManager; +import java.util.HashMap; +import java.util.Map; + +public class PoaAnalytics implements PoaEventSender { + + public static final String POA_STARTED = "POA_STARTED"; + public static final String POA_COMPLETED = "POA_COMPLETED"; + public static final String RAKAM_POA_EVENT = "wallet_poa_proof"; + private static final String EVENT_PACKAGE_NAME = "package_name"; + private static final String EVENT_CAMPAIGN_ID = "campaign_id"; + private static final String EVENT_NETWORK_ID = "network_id"; + private static final String EVENT_STATUS = "status"; + private static final String EVENT_ERROR = "error_details"; + private static final String WALLET = "WALLET"; + + private final AnalyticsManager analytics; + + public PoaAnalytics(AnalyticsManager analytics) { + this.analytics = analytics; + } + + @Override + public void sendPoaStartedEvent(String packageName, String campaignId, String networkId) { + Map eventData = new HashMap<>(); + eventData.put(EVENT_PACKAGE_NAME, packageName); + eventData.put(EVENT_CAMPAIGN_ID, campaignId); + eventData.put(EVENT_NETWORK_ID, networkId); + + sendRakamProofEvent(packageName, "started", ""); + analytics.logEvent(eventData, POA_STARTED, AnalyticsManager.Action.AUTO, WALLET); + } + + @Override + public void sendPoaCompletedEvent(String packageName, String campaignId, String networkId) { + Map eventData = new HashMap<>(); + eventData.put(EVENT_PACKAGE_NAME, packageName); + eventData.put(EVENT_CAMPAIGN_ID, campaignId); + eventData.put(EVENT_NETWORK_ID, networkId); + + analytics.logEvent(eventData, POA_COMPLETED, AnalyticsManager.Action.AUTO, WALLET); + } + + public void sendRakamProofEvent(String packageName, String status, String error) { + Map eventData = new HashMap<>(); + eventData.put(EVENT_PACKAGE_NAME, packageName); + eventData.put(EVENT_STATUS, status); + if (!TextUtils.isEmpty(error)) eventData.put(EVENT_ERROR, error); + + analytics.logEvent(eventData, RAKAM_POA_EVENT, AnalyticsManager.Action.AUTO, WALLET); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/analytics/PoaEventSender.java b/app/src/main/java/com/asfoundation/wallet/billing/analytics/PoaEventSender.java new file mode 100644 index 00000000000..51d7f17f2d3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/analytics/PoaEventSender.java @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.billing.analytics; + +interface PoaEventSender { + + void sendPoaStartedEvent(String packageName, String campaignId, String networkId); + + void sendPoaCompletedEvent(String packageName, String campaignId, String networkId); +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/analytics/WalletsAnalytics.kt b/app/src/main/java/com/asfoundation/wallet/billing/analytics/WalletsAnalytics.kt new file mode 100644 index 00000000000..e1227eddba8 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/analytics/WalletsAnalytics.kt @@ -0,0 +1,117 @@ +package com.asfoundation.wallet.billing.analytics + +import cm.aptoide.analytics.AnalyticsManager +import java.util.* + +class WalletsAnalytics(private val analytics: AnalyticsManager) : WalletsEventSender { + + override fun sendCreateBackupEvent(action: String, context: String, + status: String) { + sendCreateBackupEvent(action, context, status, null) + } + + override fun sendCreateBackupEvent(action: String, context: String, + status: String, + errorDetails: String?) { + val eventData = HashMap() + eventData[EVENT_ACTION] = action + eventData[EVENT_CONTEXT] = context + eventData[EVENT_STATUS] = status + if (errorDetails != null) eventData[EVENT_ERROR_DETAILS] = errorDetails + analytics.logEvent(eventData, WALLET_CREATE_BACKUP, AnalyticsManager.Action.CLICK, WALLET) + } + + override fun sendSaveBackupEvent(action: String) { + val eventData: MutableMap = + HashMap() + eventData[EVENT_ACTION] = action + analytics.logEvent(eventData, WALLET_SAVE_BACKUP, AnalyticsManager.Action.CLICK, WALLET) + } + + override fun sendWalletConfirmationBackupEvent(action: String) { + val eventData: MutableMap = + HashMap() + eventData[EVENT_ACTION] = action + analytics.logEvent(eventData, WALLET_CONFIRMATION_BACKUP, + AnalyticsManager.Action.CLICK, WALLET) + } + + override fun sendWalletSaveFileEvent(action: String, status: String, + errorDetails: String?) { + val eventData: MutableMap = + HashMap() + eventData[EVENT_ACTION] = action + eventData[EVENT_STATUS] = status + if (errorDetails != null) eventData[EVENT_ERROR_DETAILS] = errorDetails + analytics.logEvent(eventData, WALLET_SAVE_FILE, + AnalyticsManager.Action.CLICK, WALLET) + } + + override fun sendWalletImportRestoreEvent(action: String, status: String, + errorDetails: String?) { + val eventData: MutableMap = + HashMap() + eventData[EVENT_ACTION] = action + eventData[EVENT_STATUS] = status + if (errorDetails != null) eventData[EVENT_ERROR_DETAILS] = errorDetails + analytics.logEvent(eventData, WALLET_IMPORT_RESTORE, + AnalyticsManager.Action.CLICK, WALLET) + } + + override fun sendWalletPasswordRestoreEvent(action: String, + status: String) { + sendWalletPasswordRestoreEvent(action, status, null) + } + + override fun sendWalletPasswordRestoreEvent(action: String, status: String, + errorDetails: String?) { + val eventData: MutableMap = + HashMap() + eventData[EVENT_ACTION] = action + eventData[EVENT_STATUS] = status + if (errorDetails != null) eventData[EVENT_ERROR_DETAILS] = errorDetails + analytics.logEvent(eventData, WALLET_PASSWORD_RESTORE, + AnalyticsManager.Action.CLICK, WALLET) + } + + override fun sendWalletCompleteRestoreEvent(status: String, + errorDetails: String?) { + val eventData: MutableMap = + HashMap() + eventData[EVENT_STATUS] = status + if (errorDetails != null) eventData[EVENT_ERROR_DETAILS] = errorDetails + analytics.logEvent(eventData, WALLET_COMPLETE_RESTORE, + AnalyticsManager.Action.CLICK, WALLET) + } + + companion object { + const val ACTION_CREATE = "create" + const val ACTION_BACK = "back" + const val ACTION_SAVE = "save" + const val ACTION_FINISH = "finish" + const val ACTION_CANCEL = "cancel" + const val ACTION_IMPORT = "import" + const val ACTION_IMPORT_FROM_FILE = "import_from_file" + const val CONTEXT_CARD = "card" + const val CONTEXT_WALLET_DETAILS = "wallet_details" + const val CONTEXT_WALLET_TOOLTIP = "tooltip" + const val CONTEXT_WALLET_BALANCE = "balance" + const val CONTEXT_WALLET_SETTINGS = "settings" + const val STATUS_SUCCESS = "success" + const val STATUS_FAIL = "fail" + const val WALLET_CREATE_BACKUP = "wallet_create_backup" + const val WALLET_SAVE_BACKUP = "wallet_save_backup" + const val WALLET_CONFIRMATION_BACKUP = "wallet_confirmation_backup" + const val WALLET_SAVE_FILE = "wallet_save_file" + const val WALLET_IMPORT_RESTORE = "wallet_import_restore" + const val WALLET_PASSWORD_RESTORE = "wallet_password_restore" + const val WALLET_COMPLETE_RESTORE = "wallet_complete_restore" + const val REASON_CANCELED = "canceled" + private const val WALLET = "WALLET" + private const val EVENT_ACTION = "action" + private const val EVENT_CONTEXT = "context" + private const val EVENT_STATUS = "status" + private const val EVENT_ERROR_DETAILS = "errorDetails" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/analytics/WalletsEventSender.kt b/app/src/main/java/com/asfoundation/wallet/billing/analytics/WalletsEventSender.kt new file mode 100644 index 00000000000..533b8f2e762 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/analytics/WalletsEventSender.kt @@ -0,0 +1,27 @@ +package com.asfoundation.wallet.billing.analytics + +interface WalletsEventSender { + fun sendCreateBackupEvent(action: String, context: String, + status: String) + + fun sendCreateBackupEvent(action: String, context: String, + status: String, errorDetails: String? = null) + + fun sendSaveBackupEvent(action: String) + fun sendWalletConfirmationBackupEvent(action: String) + + fun sendWalletSaveFileEvent(action: String, status: String, + errorDetails: String? = null) + + fun sendWalletImportRestoreEvent(action: String, status: String, + errorDetails: String? = null) + + fun sendWalletPasswordRestoreEvent(action: String, + status: String) + + fun sendWalletPasswordRestoreEvent(action: String, status: String, + errorDetails: String? = null) + + fun sendWalletCompleteRestoreEvent(status: String, + errorDetails: String? = null) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/partners/AddressService.kt b/app/src/main/java/com/asfoundation/wallet/billing/partners/AddressService.kt new file mode 100644 index 00000000000..31eabcd326d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/partners/AddressService.kt @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.billing.partners + +import io.reactivex.Single + +interface AddressService { + fun getStoreAddressForPackage(packageName: String): Single + + fun getOemAddressForPackage(packageName: String): Single +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/partners/BdsPartnersApi.kt b/app/src/main/java/com/asfoundation/wallet/billing/partners/BdsPartnersApi.kt new file mode 100644 index 00000000000..301d9a4e5f9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/partners/BdsPartnersApi.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.billing.partners + +import io.reactivex.Single +import retrofit2.http.GET +import retrofit2.http.Query + + +interface BdsPartnersApi { + @GET("/roles/8.20180518/stores") + fun getStoreWallet(@Query("package.name") packageName: String?, + @Query("device.manufacturer") manufacturer: String?, + @Query("device.model") model: String?, + @Query("oemid") uid: String?): Single + + @GET("/roles/8.20180518/oems") + fun getOemWallet(@Query("package.name") packageName: String?, + @Query("device.manufacturer") manufacturer: String?, + @Query("device.model") model: String?, + @Query("oemid") uid: String?): Single +} + +data class GetWalletResponse(val items: List) + +data class Store(val user: Data) + +data class Data(val wallet_address: String) diff --git a/app/src/main/java/com/asfoundation/wallet/billing/partners/IExtractOemId.kt b/app/src/main/java/com/asfoundation/wallet/billing/partners/IExtractOemId.kt new file mode 100644 index 00000000000..38b2d4275ca --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/partners/IExtractOemId.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.billing.partners + +import io.reactivex.Single + +interface IExtractOemId { + fun extract(packageName: String): Single +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/partners/InstallerService.kt b/app/src/main/java/com/asfoundation/wallet/billing/partners/InstallerService.kt new file mode 100644 index 00000000000..c73c293db03 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/partners/InstallerService.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.billing.partners + +import io.reactivex.Single + +interface InstallerService { + fun getInstallerPackageName(appPackageName: String): Single +} diff --git a/app/src/main/java/com/asfoundation/wallet/billing/partners/InstallerSourceService.kt b/app/src/main/java/com/asfoundation/wallet/billing/partners/InstallerSourceService.kt new file mode 100644 index 00000000000..b1cd544c9e0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/partners/InstallerSourceService.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.billing.partners + +import android.content.Context +import io.reactivex.Single + +class InstallerSourceService(val context: Context) : InstallerService { + + override fun getInstallerPackageName(appPackageName: String): Single { + return try { + Single.just(context.packageManager.getInstallerPackageName(appPackageName) ?: "") + } catch (e: IllegalArgumentException) { + Single.error(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/partners/OemIdExtractorService.kt b/app/src/main/java/com/asfoundation/wallet/billing/partners/OemIdExtractorService.kt new file mode 100644 index 00000000000..723740f5451 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/partners/OemIdExtractorService.kt @@ -0,0 +1,24 @@ +package com.asfoundation.wallet.billing.partners + +import android.content.Context +import android.content.pm.PackageManager +import io.reactivex.Single + + +class OemIdExtractorService( + private val extractorV1: IExtractOemId, + private val extractorV2: IExtractOemId) { + + fun extractOemId(packageName: String): Single { + return extractorV2.extract(packageName) + .doOnSuccess { extracted -> check(extracted.isNotEmpty()) } + .onErrorResumeNext(extractorV1.extract(packageName)) + } +} + +@Throws(PackageManager.NameNotFoundException::class) +fun getPackageName(context: Context, packageName: String): String { + return context.packageManager + .getPackageInfo(packageName, 0) + .applicationInfo.sourceDir +} diff --git a/app/src/main/java/com/asfoundation/wallet/billing/partners/OemIdExtractorV1.kt b/app/src/main/java/com/asfoundation/wallet/billing/partners/OemIdExtractorV1.kt new file mode 100644 index 00000000000..c417aef54ef --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/partners/OemIdExtractorV1.kt @@ -0,0 +1,32 @@ +package com.asfoundation.wallet.billing.partners + +import android.content.Context +import io.reactivex.Single +import java.util.* +import java.util.zip.ZipFile + +class OemIdExtractorV1(private val context: Context) : + IExtractOemId { + override fun extract(packageName: String): Single { + return Single.create { + try { + var oemId = "" + val sourceDir = + getPackageName(context, packageName) + val myZipFile = ZipFile(sourceDir) + val entry = myZipFile.getEntry("META-INF/attrib") + entry?.let { + val inputStream = myZipFile.getInputStream(entry) + val properties = Properties() + properties.load(inputStream) + if (properties.containsKey("oemid")) { + oemId = properties.getProperty("oemid") + } + } + it.onSuccess(oemId) + } catch (e: Exception) { + it.onError(e) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/partners/OemIdExtractorV2.kt b/app/src/main/java/com/asfoundation/wallet/billing/partners/OemIdExtractorV2.kt new file mode 100644 index 00000000000..c765f0b6ddb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/partners/OemIdExtractorV2.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.billing.partners + +import android.content.Context +import com.aptoide.apk.injector.extractor.domain.IExtract +import io.reactivex.Single +import java.io.File + +class OemIdExtractorV2 @JvmOverloads constructor(private val context: Context, + private val extractor: IExtract) : + IExtractOemId { + + override fun extract(packageName: String): Single { + return Single.fromCallable { + getPackageName(context, packageName) + } + .flatMap { sourceDir -> + Single.fromCallable { + extractor.extract( + File(sourceDir)) + } + } + .map { + it.split(",")[0] + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/partners/PartnerAddressService.kt b/app/src/main/java/com/asfoundation/wallet/billing/partners/PartnerAddressService.kt new file mode 100644 index 00000000000..e88ccd91e83 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/partners/PartnerAddressService.kt @@ -0,0 +1,36 @@ +package com.asfoundation.wallet.billing.partners + +import com.asfoundation.wallet.util.DeviceInfo +import io.reactivex.Single +import io.reactivex.functions.BiFunction + + +class PartnerAddressService(private val installerService: InstallerService, + private val walletAddressService: WalletAddressService, + private val deviceInfo: DeviceInfo, + private val oemIdExtractorService: OemIdExtractorService) : + AddressService { + + override fun getStoreAddressForPackage(packageName: String): Single { + return Single.zip(installerService.getInstallerPackageName(packageName), + oemIdExtractorService.extractOemId(packageName), + BiFunction { installerPackage: String, oemId: String -> Pair(installerPackage, oemId) }) + .flatMap { pair -> + walletAddressService.getStoreWalletForPackage(pair.first.ifEmpty { null }, + deviceInfo.manufacturer, deviceInfo.model, pair.second.ifEmpty { null }) + } + .onErrorResumeNext { walletAddressService.getStoreDefaultAddress() } + } + + override fun getOemAddressForPackage(packageName: String): Single { + return Single.zip(installerService.getInstallerPackageName(packageName), + oemIdExtractorService.extractOemId(packageName), + BiFunction { installerPackage: String, oemId: String -> Pair(installerPackage, oemId) }) + .flatMap { pair -> + walletAddressService.getOemWalletForPackage(pair.first.ifEmpty { null }, + deviceInfo.manufacturer, deviceInfo.model, pair.second.ifEmpty { null }) + } + .onErrorResumeNext { walletAddressService.getOemDefaultAddress() } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/partners/PartnerWalletAddressService.kt b/app/src/main/java/com/asfoundation/wallet/billing/partners/PartnerWalletAddressService.kt new file mode 100644 index 00000000000..3285d437e24 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/partners/PartnerWalletAddressService.kt @@ -0,0 +1,31 @@ +package com.asfoundation.wallet.billing.partners + +import io.reactivex.Single + +class PartnerWalletAddressService(private val bdsApi: BdsPartnersApi, + private val defaultStoreAddress: String, + private val defaultOemAddress: String) : WalletAddressService { + + override fun getStoreDefaultAddress(): Single { + return Single.just(defaultStoreAddress) + } + + override fun getOemDefaultAddress(): Single { + return Single.just(defaultOemAddress) + } + + override fun getStoreWalletForPackage(packageName: String?, manufacturer: String?, model: String?, + storeId: String?): Single { + return bdsApi.getStoreWallet(packageName, manufacturer, model, storeId) + .map { it.items[0].user.wallet_address } + .onErrorReturn { defaultStoreAddress } + } + + override fun getOemWalletForPackage(packageName: String?, manufacturer: String?, + model: String?, + storeId: String?): Single { + return bdsApi.getOemWallet(packageName, manufacturer, model, storeId) + .map { it.items[0].user.wallet_address } + .onErrorReturn { defaultOemAddress } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/partners/WalletAddressService.kt b/app/src/main/java/com/asfoundation/wallet/billing/partners/WalletAddressService.kt new file mode 100644 index 00000000000..2b3f2f63203 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/partners/WalletAddressService.kt @@ -0,0 +1,16 @@ +package com.asfoundation.wallet.billing.partners + +import io.reactivex.Single + +interface WalletAddressService { + fun getStoreDefaultAddress(): Single + + fun getOemDefaultAddress(): Single + + fun getStoreWalletForPackage(packageName: String?, manufacturer: String?, model: String?, + storeId: String?): Single + + fun getOemWalletForPackage(packageName: String?, manufacturer: String?, + model: String?, + storeId: String?): Single +} diff --git a/app/src/main/java/com/asfoundation/wallet/billing/product/Product.java b/app/src/main/java/com/asfoundation/wallet/billing/product/Product.java new file mode 100644 index 00000000000..41fc4109d23 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/product/Product.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016. + * Modified by Marcelo Benites on 30/08/2016. + */ + +package com.asfoundation.wallet.billing.product; + +import com.asfoundation.wallet.billing.Price; + +public class Product { + + private final String id; + private final String icon; + private final String title; + private final String description; + private final Price price; + private final int packageVersionCode; + + public Product(String id, String icon, String title, String description, Price price, + int packageVersionCode) { + this.id = id; + this.icon = icon; + this.title = title; + this.description = description; + this.price = price; + this.packageVersionCode = packageVersionCode; + } + + public String getId() { + return id; + } + + public String getIcon() { + return icon; + } + + public String getTitle() { + return title; + } + + public Price getPrice() { + return price; + } + + public String getDescription() { + return description; + } + + public int getPackageVersionCode() { + return packageVersionCode; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/purchase/InAppDeepLinkRepository.kt b/app/src/main/java/com/asfoundation/wallet/billing/purchase/InAppDeepLinkRepository.kt new file mode 100644 index 00000000000..76e562dd3f1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/purchase/InAppDeepLinkRepository.kt @@ -0,0 +1,33 @@ +package com.asfoundation.wallet.billing.purchase + +import io.reactivex.Single + +interface InAppDeepLinkRepository { + + /** + * All optional fields should be passed despite possible being null as these are + * required by some applications to complete the purchase flow + * @param domain package name of the application + * @param skuId name of the product that is being bought + * @param userWalletAddress address of the user wallet + * @param signature signature obtained after signing the wallet + * @param originalAmount amount of the transaction. Only needed in one step payments + * @param originalCurrency currency of the transaction. Only needed in one step payments + * @param paymentMethod Name of the payment method being used + * @param developerWalletAddress Wallet address of the apps developer. Null on topup + * @param storeWalletAddress Wallet address of the store from which the app was downloaded. Null on topup + * @param oemWalletAddress Wallet address of the original equipment manufacturer. Null on topup + * @param callbackUrl url used in some purchases by the application to complete the purchase + * @param orderReference reference used in some purchases by the application to + * complete the purchase + * @param payload Group of details used in some purchases by the application to + * complete the purchase + */ + fun getDeepLink(domain: String, skuId: String?, userWalletAddress: String, + signature: String, originalAmount: String?, originalCurrency: String?, + paymentMethod: String, developerWalletAddress: String?, + storeWalletAddress: String?, oemWalletAddress: String?, + callbackUrl: String?, orderReference: String?, + payload: String?): Single + +} diff --git a/app/src/main/java/com/asfoundation/wallet/billing/purchase/LocalPaymentsLinkRepository.kt b/app/src/main/java/com/asfoundation/wallet/billing/purchase/LocalPaymentsLinkRepository.kt new file mode 100644 index 00000000000..4298dc250df --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/purchase/LocalPaymentsLinkRepository.kt @@ -0,0 +1,44 @@ +package com.asfoundation.wallet.billing.purchase + +import com.asfoundation.wallet.billing.share.GetPaymentLinkResponse +import com.google.gson.annotations.SerializedName +import io.reactivex.Single +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Query + +class LocalPaymentsLinkRepository(private var api: DeepLinkApi) : InAppDeepLinkRepository { + + override fun getDeepLink(domain: String, skuId: String?, + userWalletAddress: String, signature: String, + originalAmount: String?, originalCurrency: String?, + paymentMethod: String, + developerWalletAddress: String?, + storeWalletAddress: String?, oemWalletAddress: String?, + callbackUrl: String?, orderReference: String?, + payload: String?): Single { + return api.getDeepLink(userWalletAddress, signature, + DeepLinkData(domain, skuId, null, originalAmount, + originalCurrency, paymentMethod, developerWalletAddress, callbackUrl, payload, + orderReference, storeWalletAddress, oemWalletAddress)) + .map { it.url } + } + + interface DeepLinkApi { + + @POST("deeplink/8.20190101/inapp/product/purchases") + fun getDeepLink(@Query("wallet.address") userWalletAddress: String, @Query("wallet.signature") + signature: String, @Body data: DeepLinkData): Single + } +} + +data class DeepLinkData(@SerializedName("package") var packageName: String, + var sku: String?, + var message: String?, @SerializedName("price.value") + var amount: String?, @SerializedName("price.currency") + var currency: String?, var method: String, + @SerializedName("wallets.developer") var developerWalletAddress: String?, + @SerializedName("callback_url") var callback: String?, + var metadata: String?, var reference: String?, + @SerializedName("wallets.store") var storeWalletAddress: String?, + @SerializedName("wallets.oem") var oemWalletAddress: String?) diff --git a/app/src/main/java/com/asfoundation/wallet/billing/purchase/Purchase.java b/app/src/main/java/com/asfoundation/wallet/billing/purchase/Purchase.java new file mode 100644 index 00000000000..b40b1359779 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/purchase/Purchase.java @@ -0,0 +1,38 @@ +package com.asfoundation.wallet.billing.purchase; + +public class Purchase { + + private final Status status; + private final String productId; + private final String transactionId; + + public Purchase(Status status, String productId, String transactionId) { + this.status = status; + this.productId = productId; + this.transactionId = transactionId; + } + + public String getTransactionId() { + return transactionId; + } + + public Status getStatus() { + return status; + } + + public String getProductId() { + return productId; + } + + public boolean isNew() { + return Status.NEW.equals(status); + } + + public boolean isCompleted() { + return Status.COMPLETED.equals(status); + } + + public enum Status { + COMPLETED, FAILED, NEW + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/billing/share/BdsShareLinkRepository.kt b/app/src/main/java/com/asfoundation/wallet/billing/share/BdsShareLinkRepository.kt new file mode 100644 index 00000000000..5916e3d2070 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/share/BdsShareLinkRepository.kt @@ -0,0 +1,31 @@ +package com.asfoundation.wallet.billing.share + +import com.google.gson.annotations.SerializedName +import io.reactivex.Single +import retrofit2.http.Body +import retrofit2.http.POST + +class BdsShareLinkRepository(private var api: BdsShareLinkApi) : ShareLinkRepository { + + override fun getLink(domain: String, skuId: String?, message: String?, walletAddress: String, + originalAmount: String?, originalCurrency: String?, + paymentMethod: String): Single { + return api.getPaymentLink( + ShareLinkData(domain, skuId, walletAddress, message, originalAmount, originalCurrency, + paymentMethod)) + .map { it.url } + } + + interface BdsShareLinkApi { + + @POST("deeplink/8.20190326/topup/inapp/products") + fun getPaymentLink(@Body data: ShareLinkData): Single + } +} + +data class ShareLinkData(@SerializedName("package") var packageName: String, + var sku: String?, @SerializedName("wallet_address") + var walletAddress: String, + var message: String?, @SerializedName("price.value") + var amount: String?, @SerializedName("price.currency") + var currency: String?, var method: String) diff --git a/app/src/main/java/com/asfoundation/wallet/billing/share/GetPaymentLinkResponse.kt b/app/src/main/java/com/asfoundation/wallet/billing/share/GetPaymentLinkResponse.kt new file mode 100644 index 00000000000..4531f591ef7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/share/GetPaymentLinkResponse.kt @@ -0,0 +1,3 @@ +package com.asfoundation.wallet.billing.share + +data class GetPaymentLinkResponse(var url: String) diff --git a/app/src/main/java/com/asfoundation/wallet/billing/share/ShareLinkRepository.kt b/app/src/main/java/com/asfoundation/wallet/billing/share/ShareLinkRepository.kt new file mode 100644 index 00000000000..535c60108a2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/billing/share/ShareLinkRepository.kt @@ -0,0 +1,11 @@ +package com.asfoundation.wallet.billing.share + +import io.reactivex.Single + +interface ShareLinkRepository { + + fun getLink(domain: String, skuId: String?, message: String?, + walletAddress: String, + originalAmount: String?, + originalCurrency: String?, paymentMethod: String): Single +} diff --git a/app/src/main/java/com/asfoundation/wallet/di/AccountsManageModule.java b/app/src/main/java/com/asfoundation/wallet/di/AccountsManageModule.java deleted file mode 100644 index 2d0b73dd765..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/AccountsManageModule.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.interact.AddTokenInteract; -import com.asfoundation.wallet.interact.CreateWalletInteract; -import com.asfoundation.wallet.interact.DefaultTokenProvider; -import com.asfoundation.wallet.interact.DeleteWalletInteract; -import com.asfoundation.wallet.interact.ExportWalletInteract; -import com.asfoundation.wallet.interact.FetchWalletsInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.interact.SetDefaultWalletInteract; -import com.asfoundation.wallet.repository.PasswordStore; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import com.asfoundation.wallet.router.ImportWalletRouter; -import com.asfoundation.wallet.router.TransactionsRouter; -import com.asfoundation.wallet.viewmodel.WalletsViewModelFactory; -import dagger.Module; -import dagger.Provides; - -@Module class AccountsManageModule { - - @Provides WalletsViewModelFactory provideAccountsManageViewModelFactory( - CreateWalletInteract createWalletInteract, SetDefaultWalletInteract setDefaultWalletInteract, - DeleteWalletInteract deleteWalletInteract, FetchWalletsInteract fetchWalletsInteract, - FindDefaultWalletInteract findDefaultWalletInteract, - ExportWalletInteract exportWalletInteract, ImportWalletRouter importWalletRouter, - TransactionsRouter transactionsRouter, AddTokenInteract addTokenInteract, - DefaultTokenProvider defaultTokenProvider) { - return new WalletsViewModelFactory(createWalletInteract, setDefaultWalletInteract, - deleteWalletInteract, fetchWalletsInteract, findDefaultWalletInteract, exportWalletInteract, - importWalletRouter, transactionsRouter, addTokenInteract, defaultTokenProvider); - } - - @Provides CreateWalletInteract provideCreateAccountInteract( - WalletRepositoryType accountRepository, PasswordStore passwordStore) { - return new CreateWalletInteract(accountRepository, passwordStore); - } - - @Provides SetDefaultWalletInteract provideSetDefaultAccountInteract( - WalletRepositoryType accountRepository) { - return new SetDefaultWalletInteract(accountRepository); - } - - @Provides DeleteWalletInteract provideDeleteAccountInteract( - WalletRepositoryType accountRepository, PasswordStore store) { - return new DeleteWalletInteract(accountRepository, store); - } - - @Provides FetchWalletsInteract provideFetchAccountsInteract( - WalletRepositoryType accountRepository) { - return new FetchWalletsInteract(accountRepository); - } - - @Provides ExportWalletInteract provideExportWalletInteract(WalletRepositoryType walletRepository, - PasswordStore passwordStore) { - return new ExportWalletInteract(walletRepository, passwordStore); - } - - @Provides ImportWalletRouter provideImportAccountRouter() { - return new ImportWalletRouter(); - } - - @Provides TransactionsRouter provideTransactionsRouter() { - return new TransactionsRouter(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/ActivityBuilders.kt b/app/src/main/java/com/asfoundation/wallet/di/ActivityBuilders.kt new file mode 100644 index 00000000000..f09ea4d1070 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/ActivityBuilders.kt @@ -0,0 +1,126 @@ +package com.asfoundation.wallet.di + +import com.asfoundation.wallet.permissions.request.view.PermissionsActivity +import com.asfoundation.wallet.referrals.InviteFriendsActivity +import com.asfoundation.wallet.topup.TopUpActivity +import com.asfoundation.wallet.ui.* +import com.asfoundation.wallet.ui.backup.WalletBackupActivity +import com.asfoundation.wallet.ui.balance.QrCodeActivity +import com.asfoundation.wallet.ui.balance.RestoreWalletActivity +import com.asfoundation.wallet.ui.balance.TokenDetailsActivity +import com.asfoundation.wallet.ui.balance.TransactionDetailActivity +import com.asfoundation.wallet.ui.iab.IabActivity +import com.asfoundation.wallet.ui.iab.WebViewActivity +import com.asfoundation.wallet.ui.onboarding.OnboardingActivity +import com.asfoundation.wallet.wallet_blocked.WalletBlockedActivity +import com.asfoundation.wallet.wallet_validation.dialog.WalletValidationDialogActivity +import com.asfoundation.wallet.wallet_validation.generic.WalletValidationActivity +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class ActivityBuilders { + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindSplashModule(): SplashActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindBaseActivityModule(): BaseActivity + + @ActivityScope + @ContributesAndroidInjector(modules = [TransactionsModule::class]) + internal abstract fun bindTransactionsModule(): TransactionsActivity + + @ActivityScope + @ContributesAndroidInjector(modules = [TransactionDetailModule::class]) + internal abstract fun bindTransactionDetailModule(): TransactionDetailActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindSettingsModule(): SettingsActivity + + @ActivityScope + @ContributesAndroidInjector(modules = [SendModule::class]) + internal abstract fun bindSendModule(): SendActivity + + @ActivityScope + @ContributesAndroidInjector(modules = [MyAddressModule::class]) + internal abstract fun bindMyAddressModule(): MyAddressActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindPermissionsActivity(): PermissionsActivity + + @ActivityScope + @ContributesAndroidInjector(modules = [ConfirmationModule::class]) + internal abstract fun bindConfirmationModule(): ConfirmationActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindIabModule(): IabActivity + + @ActivityScope + @ContributesAndroidInjector(modules = [GasSettingsModule::class]) + internal abstract fun bindGasSettingsModule(): GasSettingsActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindTopUpActivity(): TopUpActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindOnboardingModule(): OnboardingActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindInviteFriendsActivity(): InviteFriendsActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindActiveWalletActivity(): QrCodeActivity + + @ContributesAndroidInjector + internal abstract fun bindWebViewActivity(): WebViewActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindWalletValidationDialogActivity(): WalletValidationDialogActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindUpdateRequiredActivity(): UpdateRequiredActivity + + @ContributesAndroidInjector + internal abstract fun bindTokenDetailsFragment(): TokenDetailsActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindWalletValidationActivity(): WalletValidationActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindWalletBlockedActivity(): WalletBlockedActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindRestoreWalletActivity(): RestoreWalletActivity + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindWalletBackupActivity(): WalletBackupActivity + + @ActivityScope + @ContributesAndroidInjector + abstract fun bindErc681Receiver(): Erc681Receiver + + @ActivityScope + @ContributesAndroidInjector + abstract fun bindOneStepPaymentReceiver(): OneStepPaymentReceiver + + @ActivityScope + @ContributesAndroidInjector + internal abstract fun bindAuthenticationPromptActivity(): AuthenticationPromptActivity + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/ActivityScope.java b/app/src/main/java/com/asfoundation/wallet/di/ActivityScope.java deleted file mode 100644 index 756c8fb7b00..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/ActivityScope.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.asfoundation.wallet.di; - -import java.lang.annotation.Retention; -import javax.inject.Scope; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Scope @Retention(RUNTIME) public @interface ActivityScope { -} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/ActivityScope.kt b/app/src/main/java/com/asfoundation/wallet/di/ActivityScope.kt new file mode 100644 index 00000000000..e146b18f0cc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/ActivityScope.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.di + +import javax.inject.Scope + +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class ActivityScope \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/AddTokenModule.java b/app/src/main/java/com/asfoundation/wallet/di/AddTokenModule.java deleted file mode 100644 index cc2e274f219..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/AddTokenModule.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.interact.AddTokenInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.router.MyTokensRouter; -import com.asfoundation.wallet.viewmodel.AddTokenViewModelFactory; -import dagger.Module; -import dagger.Provides; - -@Module public class AddTokenModule { - - @Provides AddTokenViewModelFactory addTokenViewModelFactory(AddTokenInteract addTokenInteract, - FindDefaultWalletInteract findDefaultWalletInteract, MyTokensRouter myTokensRouter) { - return new AddTokenViewModelFactory(addTokenInteract, findDefaultWalletInteract, - myTokensRouter); - } - - @Provides MyTokensRouter provideMyTokensRouter() { - return new MyTokensRouter(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/AnalyticsModule.kt b/app/src/main/java/com/asfoundation/wallet/di/AnalyticsModule.kt new file mode 100644 index 00000000000..3a4256d88ba --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/AnalyticsModule.kt @@ -0,0 +1,195 @@ +package com.asfoundation.wallet.di + +import android.content.Context +import cm.aptoide.analytics.AnalyticsManager +import com.asfoundation.wallet.advertise.PoaAnalyticsController +import com.asfoundation.wallet.analytics.* +import com.asfoundation.wallet.analytics.gamification.GamificationAnalytics +import com.asfoundation.wallet.billing.analytics.* +import com.asfoundation.wallet.identification.IdsRepository +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.topup.TopUpAnalytics +import com.asfoundation.wallet.transactions.TransactionsAnalytics +import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor +import com.asfoundation.wallet.ui.iab.LocalPaymentAnalytics +import com.asfoundation.wallet.ui.iab.PaymentMethodsAnalytics +import com.asfoundation.wallet.wallet_validation.generic.WalletValidationAnalytics +import com.facebook.appevents.AppEventsLogger +import dagger.Module +import dagger.Provides +import io.reactivex.schedulers.Schedulers +import okhttp3.OkHttpClient +import java.util.concurrent.CopyOnWriteArrayList +import javax.inject.Named +import javax.inject.Singleton + +@Module +class AnalyticsModule { + + @Provides + fun provideLocalPaymentAnalytics(billingAnalytics: BillingAnalytics, + inAppPurchaseInteractor: InAppPurchaseInteractor): LocalPaymentAnalytics { + return LocalPaymentAnalytics(billingAnalytics, inAppPurchaseInteractor, Schedulers.io()) + } + + @Singleton + @Provides + fun providesPageViewAnalytics(analyticsManager: AnalyticsManager): PageViewAnalytics { + return PageViewAnalytics(analyticsManager) + } + + @Singleton + @Provides + @Named("bi_event_list") + fun provideBiEventList() = listOf( + BillingAnalytics.PURCHASE_DETAILS, + BillingAnalytics.PAYMENT_METHOD_DETAILS, + BillingAnalytics.PAYMENT, + PoaAnalytics.POA_STARTED, + PoaAnalytics.POA_COMPLETED) + + @Singleton + @Provides + @Named("facebook_event_list") + fun provideFacebookEventList() = listOf( + BillingAnalytics.PURCHASE_DETAILS, + BillingAnalytics.PAYMENT_METHOD_DETAILS, + BillingAnalytics.PAYMENT, + BillingAnalytics.REVENUE, + PoaAnalytics.POA_STARTED, + PoaAnalytics.POA_COMPLETED, + TransactionsAnalytics.OPEN_APPLICATION, + GamificationAnalytics.GAMIFICATION, + GamificationAnalytics.GAMIFICATION_MORE_INFO + ) + + @Singleton + @Provides + @Named("rakam_event_list") + fun provideRakamEventList() = listOf( + BillingAnalytics.RAKAM_PRESELECTED_PAYMENT_METHOD, + BillingAnalytics.RAKAM_PAYMENT_METHOD, + BillingAnalytics.RAKAM_PAYMENT_CONFIRMATION, + BillingAnalytics.RAKAM_PAYMENT_CONCLUSION, + BillingAnalytics.RAKAM_PAYMENT_START, + BillingAnalytics.RAKAM_PAYPAL_URL, + TopUpAnalytics.WALLET_TOP_UP_START, + TopUpAnalytics.WALLET_TOP_UP_SELECTION, + TopUpAnalytics.WALLET_TOP_UP_CONFIRMATION, + TopUpAnalytics.WALLET_TOP_UP_CONCLUSION, + TopUpAnalytics.WALLET_TOP_UP_PAYPAL_URL, + PoaAnalytics.RAKAM_POA_EVENT, + WalletValidationAnalytics.WALLET_PHONE_NUMBER_VERIFICATION, + WalletValidationAnalytics.WALLET_CODE_VERIFICATION, + WalletValidationAnalytics.WALLET_VERIFICATION_CONFIRMATION, + WalletsAnalytics.WALLET_CREATE_BACKUP, + WalletsAnalytics.WALLET_SAVE_BACKUP, + WalletsAnalytics.WALLET_CONFIRMATION_BACKUP, + WalletsAnalytics.WALLET_SAVE_FILE, + WalletsAnalytics.WALLET_IMPORT_RESTORE, + WalletsAnalytics.WALLET_PASSWORD_RESTORE, + PageViewAnalytics.WALLET_PAGE_VIEW + ) + + @Singleton + @Provides + @Named("amplitude_event_list") + fun provideAmplitudeEventList() = listOf( + BillingAnalytics.RAKAM_PRESELECTED_PAYMENT_METHOD, + BillingAnalytics.RAKAM_PAYMENT_METHOD, + BillingAnalytics.RAKAM_PAYMENT_CONFIRMATION, + BillingAnalytics.RAKAM_PAYMENT_CONCLUSION, + BillingAnalytics.RAKAM_PAYMENT_START, + BillingAnalytics.RAKAM_PAYPAL_URL, + TopUpAnalytics.WALLET_TOP_UP_START, + TopUpAnalytics.WALLET_TOP_UP_SELECTION, + TopUpAnalytics.WALLET_TOP_UP_CONFIRMATION, + TopUpAnalytics.WALLET_TOP_UP_CONCLUSION, + TopUpAnalytics.WALLET_TOP_UP_PAYPAL_URL, + PoaAnalytics.RAKAM_POA_EVENT, + WalletValidationAnalytics.WALLET_PHONE_NUMBER_VERIFICATION, + WalletValidationAnalytics.WALLET_CODE_VERIFICATION, + WalletValidationAnalytics.WALLET_VERIFICATION_CONFIRMATION, + WalletsAnalytics.WALLET_CREATE_BACKUP, + WalletsAnalytics.WALLET_SAVE_BACKUP, + WalletsAnalytics.WALLET_CONFIRMATION_BACKUP, + WalletsAnalytics.WALLET_SAVE_FILE, + WalletsAnalytics.WALLET_IMPORT_RESTORE, + WalletsAnalytics.WALLET_PASSWORD_RESTORE, + PageViewAnalytics.WALLET_PAGE_VIEW + ) + + @Singleton + @Provides + fun provideAnalyticsManager(@Named("default") okHttpClient: OkHttpClient, api: AnalyticsAPI, + context: Context, @Named("bi_event_list") biEventList: List, + @Named("facebook_event_list") facebookEventList: List, + @Named("rakam_event_list") rakamEventList: List, + @Named("amplitude_event_list") + amplitudeEventList: List): AnalyticsManager { + return AnalyticsManager.Builder() + .addLogger(BackendEventLogger(api), biEventList) + .addLogger(FacebookEventLogger(AppEventsLogger.newLogger(context)), facebookEventList) + .addLogger(RakamEventLogger(), rakamEventList) + .addLogger(AmplitudeEventLogger(), amplitudeEventList) + .setAnalyticsNormalizer(KeysNormalizer()) + .setDebugLogger(LogcatAnalyticsLogger()) + .setKnockLogger(HttpClientKnockLogger(okHttpClient)) + .build() + } + + @Singleton + @Provides + fun provideWalletEventSender(analytics: AnalyticsManager): WalletsEventSender = + WalletsAnalytics(analytics) + + @Singleton + @Provides + fun provideBillingAnalytics(analytics: AnalyticsManager) = BillingAnalytics(analytics) + + @Singleton + @Provides + fun providePoAAnalytics(analytics: AnalyticsManager) = PoaAnalytics(analytics) + + @Singleton + @Provides + fun providesPoaAnalyticsController() = PoaAnalyticsController(CopyOnWriteArrayList()) + + @Singleton + @Provides + fun providesTransactionsAnalytics(analytics: AnalyticsManager) = TransactionsAnalytics(analytics) + + @Singleton + @Provides + fun provideGamificationAnalytics(analytics: AnalyticsManager) = GamificationAnalytics(analytics) + + @Singleton + @Provides + fun provideRakamAnalyticsSetup(context: Context, idsRepository: IdsRepository, + logger: Logger): RakamAnalytics { + return RakamAnalytics(context, idsRepository, logger) + } + + @Singleton + @Provides + fun provideAmplitudeAnalytics(context: Context, + idsRepository: IdsRepository): AmplitudeAnalytics { + return AmplitudeAnalytics(context, idsRepository) + } + + @Singleton + @Provides + fun provideTopUpAnalytics(analyticsManager: AnalyticsManager) = TopUpAnalytics(analyticsManager) + + @Singleton + @Provides + fun provideWalletValidationAnalytics(analyticsManager: AnalyticsManager) = + WalletValidationAnalytics(analyticsManager) + + @Provides + fun providePaymentMethodsAnalytics(billingAnalytics: BillingAnalytics, + rakamAnalytics: RakamAnalytics, + amplitudeAnalytics: AmplitudeAnalytics): PaymentMethodsAnalytics { + return PaymentMethodsAnalytics(billingAnalytics, rakamAnalytics, amplitudeAnalytics) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/di/AppComponent.java b/app/src/main/java/com/asfoundation/wallet/di/AppComponent.java deleted file mode 100644 index fb428afed5f..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/AppComponent.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.App; -import dagger.BindsInstance; -import dagger.Component; -import dagger.android.support.AndroidSupportInjectionModule; -import javax.inject.Singleton; - -@Singleton @Component(modules = { - AndroidSupportInjectionModule.class, ToolsModule.class, RepositoriesModule.class, - BuildersModule.class -}) public interface AppComponent { - - void inject(App app); - - @Component.Builder interface Builder { - @BindsInstance Builder application(App app); - - AppComponent build(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/AppComponent.kt b/app/src/main/java/com/asfoundation/wallet/di/AppComponent.kt new file mode 100644 index 00000000000..d74d926a405 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/AppComponent.kt @@ -0,0 +1,31 @@ +package com.asfoundation.wallet.di + +import com.asfoundation.wallet.App +import dagger.BindsInstance +import dagger.Component +import dagger.android.support.AndroidSupportInjectionModule +import javax.inject.Singleton + +@Singleton +@Component( + modules = [AndroidSupportInjectionModule::class, + AppModule::class, + RepositoryModule::class, + ActivityBuilders::class, + FragmentBuilders::class, + InteractorModule::class, + AnalyticsModule::class, + ServiceModule::class, + BroadcastReceiverBuilders::class, + ServiceBuilders::class]) +interface AppComponent { + + fun inject(app: App?) + + @Component.Builder + interface Builder { + @BindsInstance + fun application(app: App): Builder + fun build(): AppComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/AppModule.kt b/app/src/main/java/com/asfoundation/wallet/di/AppModule.kt new file mode 100644 index 00000000000..bfa78618977 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/AppModule.kt @@ -0,0 +1,507 @@ +package com.asfoundation.wallet.di + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.ContentResolver +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.os.Build +import android.preference.PreferenceManager +import androidx.biometric.BiometricManager +import androidx.core.app.NotificationCompat +import androidx.room.Room +import com.adyen.checkout.core.api.Environment +import com.appcoins.wallet.appcoins.rewards.AppcoinsRewards +import com.appcoins.wallet.appcoins.rewards.repository.BdsAppcoinsRewardsRepository +import com.appcoins.wallet.appcoins.rewards.repository.backend.BackendApi +import com.appcoins.wallet.bdsbilling.* +import com.appcoins.wallet.bdsbilling.BillingPaymentProofSubmissionImpl +import com.appcoins.wallet.bdsbilling.mappers.ExternalBillingSerializer +import com.appcoins.wallet.bdsbilling.repository.BdsApiSecondary +import com.appcoins.wallet.bdsbilling.repository.BdsRepository +import com.appcoins.wallet.bdsbilling.repository.RemoteRepository +import com.appcoins.wallet.bdsbilling.repository.RemoteRepository.BdsApi +import com.appcoins.wallet.billing.BillingMessagesMapper +import com.appcoins.wallet.commons.MemoryCache +import com.appcoins.wallet.gamification.Gamification +import com.appcoins.wallet.gamification.repository.PromotionDatabase +import com.appcoins.wallet.gamification.repository.PromotionDatabase.Companion.MIGRATION_1_2 +import com.appcoins.wallet.gamification.repository.PromotionsRepository +import com.appcoins.wallet.permissions.Permissions +import com.aptoide.apk.injector.extractor.data.Extractor +import com.aptoide.apk.injector.extractor.data.ExtractorV1 +import com.aptoide.apk.injector.extractor.data.ExtractorV2 +import com.aptoide.apk.injector.extractor.domain.IExtract +import com.asf.appcoins.sdk.contractproxy.AppCoinsAddressProxyBuilder +import com.asf.appcoins.sdk.contractproxy.AppCoinsAddressProxySdk +import com.asf.wallet.BuildConfig +import com.asf.wallet.R +import com.asfoundation.wallet.App +import com.asfoundation.wallet.C +import com.asfoundation.wallet.billing.CreditsRemoteRepository +import com.asfoundation.wallet.billing.partners.AddressService +import com.asfoundation.wallet.entity.NetworkInfo +import com.asfoundation.wallet.interact.BalanceGetter +import com.asfoundation.wallet.interact.BuildConfigDefaultTokenProvider +import com.asfoundation.wallet.interact.DefaultTokenProvider +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.logging.DebugReceiver +import com.asfoundation.wallet.logging.LogReceiver +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.logging.WalletLogger +import com.asfoundation.wallet.permissions.repository.PermissionRepository +import com.asfoundation.wallet.permissions.repository.PermissionsDatabase +import com.asfoundation.wallet.poa.* +import com.asfoundation.wallet.repository.* +import com.asfoundation.wallet.repository.IpCountryCodeProvider.IpApi +import com.asfoundation.wallet.router.GasSettingsRouter +import com.asfoundation.wallet.service.AutoUpdateService.AutoUpdateApi +import com.asfoundation.wallet.service.CampaignService +import com.asfoundation.wallet.service.ServicesErrorCodeMapper +import com.asfoundation.wallet.service.TokenRateService +import com.asfoundation.wallet.support.SupportSharedPreferences +import com.asfoundation.wallet.topup.TopUpValuesApiResponseMapper +import com.asfoundation.wallet.transactions.TransactionsMapper +import com.asfoundation.wallet.ui.airdrop.AirdropChainIdMapper +import com.asfoundation.wallet.ui.gamification.GamificationMapper +import com.asfoundation.wallet.ui.iab.* +import com.asfoundation.wallet.ui.iab.raiden.MultiWalletNonceObtainer +import com.asfoundation.wallet.ui.iab.raiden.NonceObtainerFactory +import com.asfoundation.wallet.ui.iab.raiden.Web3jNonceProvider +import com.asfoundation.wallet.util.* +import com.asfoundation.wallet.util.CurrencyFormatUtils.Companion.create +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.gson.Gson +import dagger.Module +import dagger.Provides +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.internal.schedulers.ExecutorScheduler +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import java.math.BigDecimal +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import javax.inject.Named +import javax.inject.Singleton + + +@Module +internal class AppModule { + @Provides + fun provideContext(application: App): Context = application.applicationContext + + @Singleton + @Provides + fun provideGson() = Gson() + + @Singleton + @Provides + @Named("blockchain") + fun provideBlockchainOkHttpClient(context: Context, + preferencesRepositoryType: PreferencesRepositoryType): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(UserAgentInterceptor(context, preferencesRepositoryType)) + .addInterceptor(LogInterceptor()) + .connectTimeout(15, TimeUnit.MINUTES) + .readTimeout(30, TimeUnit.MINUTES) + .writeTimeout(30, TimeUnit.MINUTES) + .build() + } + + @Singleton + @Provides + @Named("default") + fun provideDefaultOkHttpClient(context: Context, + preferencesRepositoryType: PreferencesRepositoryType): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(UserAgentInterceptor(context, preferencesRepositoryType)) + .addInterceptor(LogInterceptor()) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(45, TimeUnit.SECONDS) + .writeTimeout(45, TimeUnit.SECONDS) + .build() + } + + @Singleton + @Provides + fun passwordStore(context: Context, logger: Logger): PasswordStore { + return TrustPasswordStore(context, logger) + } + + @Singleton + @Provides + fun provideLogger(): Logger { + val receivers = ArrayList() + if (BuildConfig.DEBUG) { + receivers.add(DebugReceiver()) + } + return WalletLogger(receivers) + } + + @Singleton + @Provides + fun providesBillingPaymentProofSubmission(api: BdsApi, + walletService: WalletService, + bdsApi: BdsApiSecondary): BillingPaymentProofSubmission { + return BillingPaymentProofSubmissionImpl.Builder() + .setApi(api) + .setBdsApiSecondary(bdsApi) + .setWalletService(walletService) + .build() + } + + @Singleton + @Provides + fun provideErrorMapper() = ErrorMapper() + + @Provides + fun provideGasSettingsRouter() = GasSettingsRouter() + + @Provides + fun providePaymentMethodsMapper( + billingMessagesMapper: BillingMessagesMapper): PaymentMethodsMapper { + return PaymentMethodsMapper(billingMessagesMapper) + } + + @Provides + fun provideNonceObtainer(web3jProvider: Web3jProvider): MultiWalletNonceObtainer { + return MultiWalletNonceObtainer(NonceObtainerFactory(30000, Web3jNonceProvider(web3jProvider))) + } + + @Provides + fun provideEIPTransferParser(defaultTokenProvider: DefaultTokenProvider): EIPTransactionParser { + return EIPTransactionParser(defaultTokenProvider) + } + + @Provides + fun provideOneStepTransferParser(proxyService: ProxyService, + billing: Billing, tokenRateService: TokenRateService, + defaultTokenProvider: DefaultTokenProvider): OneStepTransactionParser { + return OneStepTransactionParser(proxyService, billing, tokenRateService, + MemoryCache(BehaviorSubject.create(), HashMap()), defaultTokenProvider) + } + + @Provides + fun provideTransferParser(eipTransactionParser: EIPTransactionParser, + oneStepTransactionParser: OneStepTransactionParser): TransferParser { + return TransferParser(eipTransactionParser, oneStepTransactionParser) + } + + @Provides + fun provideDefaultTokenProvider(findDefaultWalletInteract: FindDefaultWalletInteract, + networkInfo: NetworkInfo): DefaultTokenProvider { + return BuildConfigDefaultTokenProvider(findDefaultWalletInteract, networkInfo) + } + + @Singleton + @Provides + fun provideMessageDigest() = Calculator() + + @Singleton + @Provides + fun provideDataMapper() = DataMapper() + + @Singleton + @Provides + @Named("REGISTER_PROOF_GAS_LIMIT") + fun provideRegisterPoaGasLimit() = BigDecimal(BuildConfig.REGISTER_PROOF_GAS_LIMIT) + + @Singleton + @Provides + fun provideBdsBackEndWriter(defaultWalletInteract: FindDefaultWalletInteract, + campaignService: CampaignService): ProofWriter { + return BdsBackEndWriter(defaultWalletInteract, campaignService) + } + + @Singleton + @Provides + fun provideAdsContractAddressSdk(): AppCoinsAddressProxySdk = + AppCoinsAddressProxyBuilder().createAddressProxySdk() + + @Singleton + @Provides + fun provideHashCalculator(calculator: Calculator) = + HashCalculator(BuildConfig.LEADING_ZEROS_ON_PROOF_OF_ATTENTION, calculator) + + @Provides + @Named("MAX_NUMBER_PROOF_COMPONENTS") + fun provideMaxNumberProofComponents() = 12 + + @Provides + fun provideTaggedCompositeDisposable() = TaggedCompositeDisposable(HashMap()) + + @Provides + @Singleton + fun providesCountryCodeProvider(@Named("default") client: OkHttpClient, + gson: Gson): CountryCodeProvider { + val api = Retrofit.Builder() + .baseUrl(IpCountryCodeProvider.ENDPOINT) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(IpApi::class.java) + return IpCountryCodeProvider(api) + } + + @Provides + @Singleton + fun provideInAppPurchaseDataSaver(context: Context, operationSources: OperationSources, + appCoinsOperationRepository: AppCoinsOperationRepository): AppcoinsOperationsDataSaver { + return AppcoinsOperationsDataSaver(operationSources.sources, appCoinsOperationRepository, + AppInfoProvider(context, ImageSaver(context.filesDir + .toString() + "/app_icons/")), + Schedulers.io(), CompositeDisposable()) + } + + @Provides + fun provideOperationSources(inAppPurchaseInteractor: InAppPurchaseInteractor, + proofOfAttentionService: ProofOfAttentionService): OperationSources { + return OperationSources(inAppPurchaseInteractor, proofOfAttentionService) + } + + @Provides + fun provideAirdropChainIdMapper(networkInfo: NetworkInfo): AirdropChainIdMapper { + return AirdropChainIdMapper(networkInfo) + } + + @Singleton + @Provides + fun provideBillingFactory(walletService: WalletService, bdsRepository: BdsRepository): Billing { + return BdsBilling(bdsRepository, walletService, BillingThrowableCodeMapper()) + } + + @Singleton + @Provides + fun provideAppcoinsRewards(walletService: WalletService, billing: Billing, backendApi: BackendApi, + remoteRepository: RemoteRepository): AppcoinsRewards { + return AppcoinsRewards( + BdsAppcoinsRewardsRepository(CreditsRemoteRepository(backendApi, remoteRepository)), + object : com.appcoins.wallet.appcoins.rewards.repository.WalletService { + override fun getWalletAddress() = walletService.getWalletAddress() + + override fun signContent(content: String) = walletService.signContent(content) + }, MemoryCache(BehaviorSubject.create(), ConcurrentHashMap()), Schedulers.io(), billing, + com.appcoins.wallet.appcoins.rewards.ErrorMapper()) + } + + @Singleton + @Provides + fun provideRewardsManager(appcoinsRewards: AppcoinsRewards, billing: Billing, + addressService: AddressService): RewardsManager { + return RewardsManager(appcoinsRewards, billing, addressService) + } + + @Singleton + @Provides + fun provideBillingMessagesMapper() = BillingMessagesMapper(ExternalBillingSerializer()) + + @Provides + fun provideAdyenEnvironment(): Environment { + return if (BuildConfig.DEBUG) { + Environment.TEST + } else { + Environment.EUROPE + } + } + + @Singleton + @Provides + fun provideSharedPreferences(context: Context): SharedPreferences { + return PreferenceManager.getDefaultSharedPreferences(context) + } + + @Provides + fun provideGamification(promotionsRepository: PromotionsRepository) = + Gamification(promotionsRepository) + + @Singleton + @Provides + fun provideBalanceGetter(appcoinsRewards: AppcoinsRewards): BalanceGetter { + return object : BalanceGetter { + override fun getBalance(address: String): Single { + return appcoinsRewards.getBalance(address) + .subscribeOn(Schedulers.io()) + } + + override fun getBalance(): Single { + return Single.just(BigDecimal.ZERO) + } + } + } + + @Singleton + @Provides + fun providesPermissions(context: Context): Permissions { + return Permissions(PermissionRepository(Room.databaseBuilder(context.applicationContext, + PermissionsDatabase::class.java, + "permissions_database") + .build() + .permissionsDao())) + } + + @Singleton + @Provides + fun providesPromotionDatabase(context: Context): PromotionDatabase { + return Room.databaseBuilder(context, PromotionDatabase::class.java, "promotion_database") + .addMigrations(MIGRATION_1_2) + .build() + } + + @Singleton + @Provides + fun providesPromotionDao(promotionDatabase: PromotionDatabase) = + promotionDatabase.promotionDao() + + @Singleton + @Provides + fun providesLevelsDao(promotionDatabase: PromotionDatabase) = + promotionDatabase.levelsDao() + + @Singleton + @Provides + fun providesLevelDao(promotionDatabase: PromotionDatabase) = + promotionDatabase.levelDao() + + @Provides + fun providesObjectMapper(): ObjectMapper { + return ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + } + + @Provides + fun providesTopUpValuesApiResponseMapper() = TopUpValuesApiResponseMapper() + + @Provides + fun providesOffChainTransactions(repository: OffChainTransactionsRepository, + mapper: TransactionsMapper): OffChainTransactions { + return OffChainTransactions(repository, mapper, versionCode) + } + + private val versionCode: String + get() = BuildConfig.VERSION_CODE.toString() + + @Provides + fun provideTransactionsMapper() = TransactionsMapper() + + @Singleton + @Provides + fun provideNotificationManager(context: Context): NotificationManager { + return context.applicationContext.getSystemService( + Context.NOTIFICATION_SERVICE) as NotificationManager + } + + @Singleton + @Provides + @Named("heads_up") + fun provideHeadsUpNotificationBuilder(context: Context, + notificationManager: NotificationManager): NotificationCompat.Builder { + val builder: NotificationCompat.Builder + val channelId = "notification_channel_heads_up_id" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channelName: CharSequence = "Notification channel" + val importance = NotificationManager.IMPORTANCE_HIGH + val notificationChannel = + NotificationChannel(channelId, channelName, importance) + builder = NotificationCompat.Builder(context, channelId) + notificationManager.createNotificationChannel(notificationChannel) + } else { + builder = NotificationCompat.Builder(context, channelId) + builder.setVibrate(LongArray(0)) + } + return builder.setContentTitle(context.getString(R.string.app_name)) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setAutoCancel(true) + } + + @Singleton + @Provides + fun provideIExtract(): IExtract { + return Extractor(ExtractorV1(), ExtractorV2()) + } + + @Singleton + @Provides + fun providePackageManager(context: Context): PackageManager = context.packageManager + + @Singleton + @Provides + fun provideAutoUpdateApi(@Named("default") client: OkHttpClient, gson: Gson): AutoUpdateApi { + val baseUrl = BuildConfig.BACKEND_HOST + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(AutoUpdateApi::class.java) + } + + @Provides + @Named("local_version_code") + fun provideLocalVersionCode(context: Context, packageManager: PackageManager): Int { + return try { + packageManager.getPackageInfo(context.packageName, 0) + .versionCode + } catch (e: PackageManager.NameNotFoundException) { + -1 + } + } + + @Singleton + @Provides + fun provideSupportSharedPreferences(preferences: SharedPreferences) = + SupportSharedPreferences(preferences) + + @Singleton + @Provides + fun provideCurrencyFormatUtils() = create() + + @Provides + fun provideContentResolver(context: Context): ContentResolver = context.contentResolver + + @Singleton + @Provides + fun providesWeb3jProvider(@Named("blockchain") client: OkHttpClient, + networkInfo: NetworkInfo): Web3jProvider { + return Web3jProvider(client, networkInfo) + } + + @Singleton + @Provides + fun providesDefaultNetwork(): NetworkInfo { + return if (BuildConfig.DEBUG) { + NetworkInfo(C.ROPSTEN_NETWORK_NAME, C.ETH_SYMBOL, + "https://ropsten.infura.io/v3/${BuildConfig.INFURA_API_KEY_ROPSTEN}", + "https://ropsten.trustwalletapp.com/", "https://ropsten.etherscan.io/tx/", 3, false) + } else { + NetworkInfo(C.ETHEREUM_NETWORK_NAME, C.ETH_SYMBOL, + "https://mainnet.infura.io/v3/${BuildConfig.INFURA_API_KEY_MAIN}", + "https://api.trustwalletapp.com/", "https://etherscan.io/tx/", 1, true) + } + } + + @Singleton + @Provides + fun providesExecutorScheduler() = ExecutorScheduler(SyncExecutor(1), false) + + @Singleton + @Provides + fun providesGamificationMapper(context: Context) = GamificationMapper(context) + + @Singleton + @Provides + fun providesServicesErrorMapper() = ServicesErrorCodeMapper() + + @Singleton + @Provides + fun providesBiometricManager(context: Context) = BiometricManager.from(context) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/BroadcastReceiverBuilders.kt b/app/src/main/java/com/asfoundation/wallet/di/BroadcastReceiverBuilders.kt new file mode 100644 index 00000000000..ba869d99e90 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/BroadcastReceiverBuilders.kt @@ -0,0 +1,17 @@ +package com.asfoundation.wallet.di + +import com.asfoundation.wallet.backup.BackupBroadcastReceiver +import com.asfoundation.wallet.support.AlarmManagerBroadcastReceiver +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class BroadcastReceiverBuilders { + + @ContributesAndroidInjector + abstract fun contributesAlarmManagerBroadcastReceiver(): AlarmManagerBroadcastReceiver + + @ContributesAndroidInjector + abstract fun contributesBackupBroadcastReceiver(): BackupBroadcastReceiver + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/BuildersModule.java b/app/src/main/java/com/asfoundation/wallet/di/BuildersModule.java deleted file mode 100644 index 53065a553b6..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/BuildersModule.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.advertise.WalletPoAService; -import com.asfoundation.wallet.ui.AddTokenActivity; -import com.asfoundation.wallet.ui.ConfirmationActivity; -import com.asfoundation.wallet.ui.Erc681Receiver; -import com.asfoundation.wallet.ui.GasSettingsActivity; -import com.asfoundation.wallet.ui.ImportWalletActivity; -import com.asfoundation.wallet.ui.MyAddressActivity; -import com.asfoundation.wallet.ui.SendActivity; -import com.asfoundation.wallet.ui.SettingsActivity; -import com.asfoundation.wallet.ui.SplashActivity; -import com.asfoundation.wallet.ui.TokenChangeCollectionActivity; -import com.asfoundation.wallet.ui.TokensActivity; -import com.asfoundation.wallet.ui.TransactionDetailActivity; -import com.asfoundation.wallet.ui.TransactionsActivity; -import com.asfoundation.wallet.ui.WalletsActivity; -import com.asfoundation.wallet.ui.airdrop.AirdropFragment; -import com.asfoundation.wallet.ui.iab.IabActivity; -import dagger.Module; -import dagger.android.ContributesAndroidInjector; - -@Module public abstract class BuildersModule { - @ActivityScope @ContributesAndroidInjector(modules = SplashModule.class) - abstract SplashActivity bindSplashModule(); - - @ActivityScope @ContributesAndroidInjector(modules = AccountsManageModule.class) - abstract WalletsActivity bindManageWalletsModule(); - - @ActivityScope @ContributesAndroidInjector(modules = ImportModule.class) - abstract ImportWalletActivity bindImportWalletModule(); - - @ActivityScope @ContributesAndroidInjector(modules = TransactionsModule.class) - abstract TransactionsActivity bindTransactionsModule(); - - @ActivityScope @ContributesAndroidInjector(modules = TransactionDetailModule.class) - abstract TransactionDetailActivity bindTransactionDetailModule(); - - @ActivityScope @ContributesAndroidInjector(modules = SettingsModule.class) - abstract SettingsActivity bindSettingsModule(); - - @ActivityScope @ContributesAndroidInjector(modules = SendModule.class) - abstract SendActivity bindSendModule(); - - @ActivityScope @ContributesAndroidInjector(modules = ConfirmationModule.class) - abstract ConfirmationActivity bindConfirmationModule(); - - @ActivityScope @ContributesAndroidInjector(modules = ConfirmationModule.class) - abstract IabActivity bindIabModule(); - - @ContributesAndroidInjector abstract MyAddressActivity bindMyAddressModule(); - - @ActivityScope @ContributesAndroidInjector(modules = TokensModule.class) - abstract TokensActivity bindTokensModule(); - - @ActivityScope @ContributesAndroidInjector(modules = GasSettingsModule.class) - abstract GasSettingsActivity bindGasSettingsModule(); - - @ActivityScope @ContributesAndroidInjector(modules = AddTokenModule.class) - abstract AddTokenActivity bindAddTokenActivity(); - - @ActivityScope @ContributesAndroidInjector(modules = ChangeTokenModule.class) - abstract TokenChangeCollectionActivity bindChangeTokenCollectionActivity(); - - @ActivityScope @ContributesAndroidInjector(modules = ConfirmationModule.class) - abstract Erc681Receiver bindErc681Receiver(); - - @ContributesAndroidInjector() abstract WalletPoAService bindWalletPoAService(); - - @ContributesAndroidInjector() abstract AirdropFragment bindAirdropFragment(); -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/ChangeTokenModule.java b/app/src/main/java/com/asfoundation/wallet/di/ChangeTokenModule.java deleted file mode 100644 index a254af5a506..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/ChangeTokenModule.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.interact.ChangeTokenEnableInteract; -import com.asfoundation.wallet.interact.DeleteTokenInteract; -import com.asfoundation.wallet.interact.FetchAllTokenInfoInteract; -import com.asfoundation.wallet.repository.TokenRepositoryType; -import com.asfoundation.wallet.viewmodel.TokenChangeCollectionViewModelFactory; -import dagger.Module; -import dagger.Provides; - -@Module class ChangeTokenModule { - - @Provides TokenChangeCollectionViewModelFactory provideChangeTokenCollectionViewModelFactory( - FetchAllTokenInfoInteract fetchAllTokenInfoInteract, - ChangeTokenEnableInteract changeTokenEnableInteract, - DeleteTokenInteract deleteTokenInteract) { - return new TokenChangeCollectionViewModelFactory(fetchAllTokenInfoInteract, - changeTokenEnableInteract, deleteTokenInteract); - } - - @Provides FetchAllTokenInfoInteract provideFetchAllTokenInfoInteract( - TokenRepositoryType tokenRepository) { - return new FetchAllTokenInfoInteract(tokenRepository); - } - - @Provides ChangeTokenEnableInteract provideChangeTokenEnableInteract( - TokenRepositoryType tokenRepository) { - return new ChangeTokenEnableInteract(tokenRepository); - } - - @Provides DeleteTokenInteract provideDeleteTokenInteract(TokenRepositoryType tokenRepository) { - return new DeleteTokenInteract(tokenRepository); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/ConfirmationModule.java b/app/src/main/java/com/asfoundation/wallet/di/ConfirmationModule.java deleted file mode 100644 index 268a3be53a1..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/ConfirmationModule.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.interact.SendTransactionInteract; -import com.asfoundation.wallet.router.GasSettingsRouter; -import com.asfoundation.wallet.viewmodel.ConfirmationViewModelFactory; -import dagger.Module; -import dagger.Provides; - -@Module(includes = { SendModule.class }) public class ConfirmationModule { - - @Provides ConfirmationViewModelFactory provideConfirmationViewModelFactory( - SendTransactionInteract sendTransactionInteract, GasSettingsRouter gasSettingsRouter) { - return new ConfirmationViewModelFactory(sendTransactionInteract, gasSettingsRouter); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/ConfirmationModule.kt b/app/src/main/java/com/asfoundation/wallet/di/ConfirmationModule.kt new file mode 100644 index 00000000000..f67e948588b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/ConfirmationModule.kt @@ -0,0 +1,22 @@ +package com.asfoundation.wallet.di + +import com.asfoundation.wallet.interact.FetchGasSettingsInteract +import com.asfoundation.wallet.interact.SendTransactionInteract +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.router.GasSettingsRouter +import com.asfoundation.wallet.viewmodel.ConfirmationViewModelFactory +import dagger.Module +import dagger.Provides + +@Module(includes = [SendModule::class]) +class ConfirmationModule { + + @Provides + fun provideConfirmationViewModelFactory(sendTransactionInteract: SendTransactionInteract, + gasSettingsRouter: GasSettingsRouter, + gasSettingsInteract: FetchGasSettingsInteract, + logger: Logger) = + ConfirmationViewModelFactory(sendTransactionInteract, gasSettingsRouter, gasSettingsInteract, + logger) + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/FragmentBuilders.kt b/app/src/main/java/com/asfoundation/wallet/di/FragmentBuilders.kt new file mode 100644 index 00000000000..ecd9696cb0d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/FragmentBuilders.kt @@ -0,0 +1,204 @@ +package com.asfoundation.wallet.di + +import com.asfoundation.wallet.billing.address.BillingAddressFragment +import com.asfoundation.wallet.billing.adyen.AdyenPaymentFragment +import com.asfoundation.wallet.permissions.manage.view.PermissionsListFragment +import com.asfoundation.wallet.permissions.request.view.CreateWalletFragment +import com.asfoundation.wallet.permissions.request.view.PermissionFragment +import com.asfoundation.wallet.promotions.PromotionsFragment +import com.asfoundation.wallet.referrals.InviteFriendsFragment +import com.asfoundation.wallet.referrals.InviteFriendsVerificationFragment +import com.asfoundation.wallet.referrals.ReferralsFragment +import com.asfoundation.wallet.topup.LocalTopUpPaymentFragment +import com.asfoundation.wallet.topup.TopUpFragment +import com.asfoundation.wallet.topup.TopUpSuccessFragment +import com.asfoundation.wallet.topup.address.BillingAddressTopUpFragment +import com.asfoundation.wallet.topup.payment.AdyenTopUpFragment +import com.asfoundation.wallet.ui.AuthenticationErrorFragment +import com.asfoundation.wallet.ui.SettingsFragment +import com.asfoundation.wallet.ui.SettingsWalletsBottomSheetFragment +import com.asfoundation.wallet.ui.airdrop.AirdropFragment +import com.asfoundation.wallet.ui.backup.BackupCreationFragment +import com.asfoundation.wallet.ui.backup.BackupSuccessFragment +import com.asfoundation.wallet.ui.backup.BackupWalletFragment +import com.asfoundation.wallet.ui.balance.BalanceFragment +import com.asfoundation.wallet.ui.balance.RestoreWalletFragment +import com.asfoundation.wallet.ui.balance.RestoreWalletPasswordFragment +import com.asfoundation.wallet.ui.gamification.GamificationFragment +import com.asfoundation.wallet.ui.iab.* +import com.asfoundation.wallet.ui.iab.share.SharePaymentLinkFragment +import com.asfoundation.wallet.ui.transact.AppcoinsCreditsTransferSuccessFragment +import com.asfoundation.wallet.ui.transact.TransferFragment +import com.asfoundation.wallet.ui.wallets.RemoveWalletFragment +import com.asfoundation.wallet.ui.wallets.WalletDetailsFragment +import com.asfoundation.wallet.ui.wallets.WalletRemoveConfirmationFragment +import com.asfoundation.wallet.ui.wallets.WalletsFragment +import com.asfoundation.wallet.wallet_validation.dialog.CodeValidationDialogFragment +import com.asfoundation.wallet.wallet_validation.dialog.PhoneValidationDialogFragment +import com.asfoundation.wallet.wallet_validation.dialog.ValidationLoadingDialogFragment +import com.asfoundation.wallet.wallet_validation.dialog.ValidationSuccessDialogFragment +import com.asfoundation.wallet.wallet_validation.generic.CodeValidationFragment +import com.asfoundation.wallet.wallet_validation.generic.PhoneValidationFragment +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class FragmentBuilders { + + @ContributesAndroidInjector + abstract fun bindAirdropFragment(): AirdropFragment + + @ContributesAndroidInjector + abstract fun bindRegularBuyFragment(): OnChainBuyFragment + + @ContributesAndroidInjector + abstract fun bindWebViewFragment(): BillingWebViewFragment + + @ContributesAndroidInjector + abstract fun bindAppcoinsRewardsBuyFragment(): AppcoinsRewardsBuyFragment + + @ContributesAndroidInjector + abstract fun bindPaymentMethodsFragment(): PaymentMethodsFragment + + @ContributesAndroidInjector + abstract fun bindPermissionFragment(): PermissionFragment + + @ContributesAndroidInjector + abstract fun bindCreateWalletFragment(): CreateWalletFragment + + @ContributesAndroidInjector + abstract fun bindPermissionsListFragment(): PermissionsListFragment + + @ContributesAndroidInjector(modules = [ConfirmationModule::class]) + abstract fun bindTransactFragment(): TransferFragment + + @ContributesAndroidInjector + abstract fun bindAppcoinsCreditsTransactSuccessFragment(): AppcoinsCreditsTransferSuccessFragment + + @ContributesAndroidInjector + abstract fun bindTopUpFragment(): TopUpFragment + + @ContributesAndroidInjector + abstract fun bindTopUpSuccessFragment(): TopUpSuccessFragment + + @ContributesAndroidInjector + abstract fun bindSharePaymentLinkFragment(): SharePaymentLinkFragment + + @ContributesAndroidInjector + abstract fun bindLocalPaymentFragment(): LocalPaymentFragment + + @ContributesAndroidInjector + abstract fun bindMergedAppcoinsFragment(): MergedAppcoinsFragment + + + @ContributesAndroidInjector + abstract fun bindPoaPhoneValidationFragment(): PhoneValidationDialogFragment + + @ContributesAndroidInjector + abstract fun bindPoaCodeValidationFragment(): CodeValidationDialogFragment + + @ContributesAndroidInjector + abstract fun bindPoaValidationLoadingFragment(): ValidationLoadingDialogFragment + + @ContributesAndroidInjector + abstract fun bindPoaValidationSuccessFragment(): ValidationSuccessDialogFragment + + @ContributesAndroidInjector + abstract fun bindBalanceFragment(): BalanceFragment + + @ContributesAndroidInjector + abstract fun bindPhoneValidationFragment(): PhoneValidationFragment + + @ContributesAndroidInjector + abstract fun bindCodeValidationFragment(): CodeValidationFragment + + @ContributesAndroidInjector + abstract fun bindPromotionsFragment(): PromotionsFragment + + @ContributesAndroidInjector + abstract fun bindInviteFriendsVerificationFragment(): InviteFriendsVerificationFragment + + @ContributesAndroidInjector + abstract fun bindInviteFriendsFragment(): InviteFriendsFragment + + @ContributesAndroidInjector + abstract fun bindReferralsFragment(): ReferralsFragment + + @ContributesAndroidInjector + abstract fun bindEarnAppcoinsFragment(): EarnAppcoinsFragment + + @ContributesAndroidInjector + abstract fun bindIabUpdateRequiredFragment(): IabUpdateRequiredFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindWalletsFragment(): WalletsFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindWalletDetailFragment(): WalletDetailsFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindAdyenPaymentFragment(): AdyenPaymentFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindAdyenTopUpFragment(): AdyenTopUpFragment + + + @ContributesAndroidInjector + abstract fun bindRemoveWalletFragment(): RemoveWalletFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindWalletRemoveConfirmationFragment(): WalletRemoveConfirmationFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindRestoreWalletFragment(): RestoreWalletFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindRestoreWalletPasswordFragment(): RestoreWalletPasswordFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindBackupWalletFragment(): BackupWalletFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindBackupCreationFragment(): BackupCreationFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindBackupSuccessFragment(): BackupSuccessFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindSettingsFragment(): SettingsFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindSettingsBottomSheetFragment(): SettingsWalletsBottomSheetFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindGamificationFragment(): GamificationFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindLocalTopUpPaymentFragment(): LocalTopUpPaymentFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindBillingAddressFragment(): BillingAddressFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindBillingAddressTopUpFragment(): BillingAddressTopUpFragment + + @FragmentScope + @ContributesAndroidInjector + abstract fun bindAuthenticationErrorFragment(): AuthenticationErrorFragment +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/FragmentScope.java b/app/src/main/java/com/asfoundation/wallet/di/FragmentScope.java deleted file mode 100644 index f7bb2d1fb46..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/FragmentScope.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.asfoundation.wallet.di; - -import java.lang.annotation.Retention; -import javax.inject.Scope; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Scope @Retention(RUNTIME) public @interface FragmentScope { -} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/FragmentScope.kt b/app/src/main/java/com/asfoundation/wallet/di/FragmentScope.kt new file mode 100644 index 00000000000..00c6d8682fc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/FragmentScope.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.di + +import javax.inject.Scope + +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class FragmentScope \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/GasSettingsModule.java b/app/src/main/java/com/asfoundation/wallet/di/GasSettingsModule.java deleted file mode 100644 index 32f9361c187..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/GasSettingsModule.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.interact.FindDefaultNetworkInteract; -import com.asfoundation.wallet.viewmodel.GasSettingsViewModelFactory; -import dagger.Module; -import dagger.Provides; - -@Module public class GasSettingsModule { - - @Provides public GasSettingsViewModelFactory provideGasSettingsViewModelFactory( - FindDefaultNetworkInteract findDefaultNetworkInteract) { - return new GasSettingsViewModelFactory(findDefaultNetworkInteract); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/GasSettingsModule.kt b/app/src/main/java/com/asfoundation/wallet/di/GasSettingsModule.kt new file mode 100644 index 00000000000..5f036be9608 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/GasSettingsModule.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.di + +import com.asfoundation.wallet.interact.FindDefaultNetworkInteract +import com.asfoundation.wallet.viewmodel.GasSettingsViewModelFactory +import dagger.Module +import dagger.Provides + +@Module +class GasSettingsModule { + @Provides + fun provideGasSettingsViewModelFactory(findDefaultNetworkInteract: FindDefaultNetworkInteract) = + GasSettingsViewModelFactory(findDefaultNetworkInteract) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/ImportModule.java b/app/src/main/java/com/asfoundation/wallet/di/ImportModule.java deleted file mode 100644 index bc0d19bed93..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/ImportModule.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.interact.ImportWalletInteract; -import com.asfoundation.wallet.repository.PasswordStore; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import com.asfoundation.wallet.viewmodel.ImportWalletViewModelFactory; -import dagger.Module; -import dagger.Provides; - -@Module class ImportModule { - @Provides ImportWalletViewModelFactory provideImportWalletViewModelFactory( - ImportWalletInteract importWalletInteract) { - return new ImportWalletViewModelFactory(importWalletInteract); - } - - @Provides ImportWalletInteract provideImportWalletInteract(WalletRepositoryType walletRepository, - PasswordStore passwordStore) { - return new ImportWalletInteract(walletRepository, passwordStore); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/InteractorModule.kt b/app/src/main/java/com/asfoundation/wallet/di/InteractorModule.kt new file mode 100644 index 00000000000..4aa425cb585 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/InteractorModule.kt @@ -0,0 +1,545 @@ +package com.asfoundation.wallet.di + +import android.content.ContentResolver +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.os.Build +import androidx.biometric.BiometricManager +import com.appcoins.wallet.appcoins.rewards.AppcoinsRewards +import com.appcoins.wallet.bdsbilling.Billing +import com.appcoins.wallet.bdsbilling.BillingPaymentProofSubmission +import com.appcoins.wallet.bdsbilling.WalletService +import com.appcoins.wallet.bdsbilling.mappers.ExternalBillingSerializer +import com.appcoins.wallet.bdsbilling.repository.BdsRepository +import com.appcoins.wallet.bdsbilling.repository.RemoteRepository +import com.appcoins.wallet.billing.BillingMessagesMapper +import com.appcoins.wallet.billing.adyen.AdyenPaymentRepository +import com.appcoins.wallet.commons.MemoryCache +import com.appcoins.wallet.gamification.Gamification +import com.appcoins.wallet.gamification.repository.PromotionsRepository +import com.appcoins.wallet.permissions.Permissions +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.Airdrop +import com.asfoundation.wallet.AirdropService +import com.asfoundation.wallet.App +import com.asfoundation.wallet.advertise.AdvertisingThrowableCodeMapper +import com.asfoundation.wallet.advertise.CampaignInteract +import com.asfoundation.wallet.backup.BackupInteract +import com.asfoundation.wallet.backup.BackupInteractContract +import com.asfoundation.wallet.backup.FileInteractor +import com.asfoundation.wallet.billing.adyen.AdyenPaymentInteractor +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.billing.partners.AddressService +import com.asfoundation.wallet.billing.purchase.InAppDeepLinkRepository +import com.asfoundation.wallet.billing.share.ShareLinkRepository +import com.asfoundation.wallet.entity.NetworkInfo +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.interact.* +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.permissions.PermissionsInteractor +import com.asfoundation.wallet.promotions.PromotionsInteractor +import com.asfoundation.wallet.promotions.PromotionsInteractorContract +import com.asfoundation.wallet.referrals.ReferralInteractor +import com.asfoundation.wallet.referrals.ReferralInteractorContract +import com.asfoundation.wallet.referrals.SharedPreferencesReferralLocalData +import com.asfoundation.wallet.repository.* +import com.asfoundation.wallet.service.CampaignService +import com.asfoundation.wallet.service.LocalCurrencyConversionService +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.support.SupportSharedPreferences +import com.asfoundation.wallet.topup.TopUpInteractor +import com.asfoundation.wallet.topup.TopUpLimitValues +import com.asfoundation.wallet.topup.TopUpValuesService +import com.asfoundation.wallet.ui.FingerPrintInteractor +import com.asfoundation.wallet.ui.SettingsInteractor +import com.asfoundation.wallet.ui.airdrop.AirdropChainIdMapper +import com.asfoundation.wallet.ui.airdrop.AirdropInteractor +import com.asfoundation.wallet.ui.airdrop.AppcoinsTransactionService +import com.asfoundation.wallet.ui.balance.BalanceInteract +import com.asfoundation.wallet.ui.balance.BalanceRepository +import com.asfoundation.wallet.ui.balance.RestoreWalletPasswordInteractor +import com.asfoundation.wallet.ui.gamification.GamificationInteractor +import com.asfoundation.wallet.ui.gamification.GamificationMapper +import com.asfoundation.wallet.ui.iab.* +import com.asfoundation.wallet.ui.iab.share.ShareLinkInteractor +import com.asfoundation.wallet.ui.onboarding.OnboardingInteract +import com.asfoundation.wallet.ui.transact.TransactionDataValidator +import com.asfoundation.wallet.ui.transact.TransferInteractor +import com.asfoundation.wallet.ui.wallets.WalletDetailsInteractor +import com.asfoundation.wallet.ui.wallets.WalletsInteract +import com.asfoundation.wallet.util.TransferParser +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import com.asfoundation.wallet.wallet_blocked.WalletStatusRepository +import com.google.gson.Gson +import dagger.Module +import dagger.Provides +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.internal.schedulers.ExecutorScheduler +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import java.math.BigDecimal +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Named +import javax.inject.Singleton + +@Module +class InteractorModule { + + @Provides + @Named("APPROVE_SERVICE_ON_CHAIN") + fun provideApproveService(sendTransactionInteract: SendTransactionInteract, + errorMapper: ErrorMapper, @Named("no_wait_transaction") + noWaitPendingTransactionService: TrackTransactionService): ApproveService { + return ApproveService(WatchedTransactionService(object : TransactionSender { + override fun send(transactionBuilder: TransactionBuilder): Single { + return sendTransactionInteract.approve(transactionBuilder) + } + }, MemoryCache(BehaviorSubject.create(), ConcurrentHashMap()), errorMapper, Schedulers.io(), + noWaitPendingTransactionService), NoValidateTransactionValidator()) + } + + @Provides + fun provideFetchGasSettingsInteract( + gasSettingsRepository: GasSettingsRepositoryType): FetchGasSettingsInteract { + return FetchGasSettingsInteract(gasSettingsRepository, Schedulers.io(), + AndroidSchedulers.mainThread()) + } + + @Provides + fun provideFindDefaultWalletInteract( + walletRepository: WalletRepositoryType): FindDefaultWalletInteract { + return FindDefaultWalletInteract(walletRepository, Schedulers.io()) + } + + @Provides + fun provideGetDefaultWalletBalance(walletRepository: WalletRepositoryType, + defaultWalletInteract: FindDefaultWalletInteract, + fetchCreditsInteract: FetchCreditsInteract, + networkInfo: NetworkInfo, + tokenRepositoryType: TokenRepositoryType): GetDefaultWalletBalanceInteract { + return GetDefaultWalletBalanceInteract(walletRepository, defaultWalletInteract, + fetchCreditsInteract, networkInfo, tokenRepositoryType) + } + + + @Provides + fun provideWalletBlockedInteract(findDefaultWalletInteract: FindDefaultWalletInteract, + walletStatusRepository: WalletStatusRepository): WalletBlockedInteract { + return WalletBlockedInteract(findDefaultWalletInteract, walletStatusRepository) + } + + @Provides + fun provideSendTransactionInteract(transactionRepository: TransactionRepositoryType, + passwordStore: PasswordStore): SendTransactionInteract { + return SendTransactionInteract(transactionRepository, passwordStore) + } + + @Singleton + @Provides + fun provideBdsInAppPurchaseInteractor( + billingPaymentProofSubmission: BillingPaymentProofSubmission, + @Named("ASF_BDS_IN_APP_INTERACTOR") inAppPurchaseInteractor: AsfInAppPurchaseInteractor, + billing: Billing): BdsInAppPurchaseInteractor { + return BdsInAppPurchaseInteractor(inAppPurchaseInteractor, billingPaymentProofSubmission, + ApproveKeyProvider(billing), billing) + } + + @Singleton + @Provides + @Named("ASF_BDS_IN_APP_INTERACTOR") + fun provideAsfBdsInAppPurchaseInteractor( + @Named("IN_APP_PURCHASE_SERVICE") inAppPurchaseService: InAppPurchaseService, + defaultWalletInteract: FindDefaultWalletInteract, + gasSettingsInteract: FetchGasSettingsInteract, parser: TransferParser, billing: Billing, + currencyConversionService: CurrencyConversionService, + bdsTransactionService: BdsTransactionService, + billingMessagesMapper: BillingMessagesMapper): AsfInAppPurchaseInteractor { + return AsfInAppPurchaseInteractor(inAppPurchaseService, defaultWalletInteract, + gasSettingsInteract, BigDecimal(BuildConfig.PAYMENT_GAS_LIMIT), parser, + billingMessagesMapper, billing, currencyConversionService, bdsTransactionService, + Schedulers.io()) + } + + @Singleton + @Provides + @Named("ASF_IN_APP_INTERACTOR") + fun provideAsfInAppPurchaseInteractor( + @Named("ASF_IN_APP_PURCHASE_SERVICE") inAppPurchaseService: InAppPurchaseService, + defaultWalletInteract: FindDefaultWalletInteract, + gasSettingsInteract: FetchGasSettingsInteract, + parser: TransferParser, billing: Billing, + currencyConversionService: CurrencyConversionService, + bdsTransactionService: BdsTransactionService, + billingMessagesMapper: BillingMessagesMapper): AsfInAppPurchaseInteractor { + return AsfInAppPurchaseInteractor(inAppPurchaseService, defaultWalletInteract, + gasSettingsInteract, BigDecimal(BuildConfig.PAYMENT_GAS_LIMIT), parser, + billingMessagesMapper, billing, currencyConversionService, bdsTransactionService, + Schedulers.io()) + } + + @Singleton + @Provides + fun provideDualInAppPurchaseInteractor(bdsInAppPurchaseInteractor: BdsInAppPurchaseInteractor, + @Named("ASF_IN_APP_INTERACTOR") + asfInAppPurchaseInteractor: AsfInAppPurchaseInteractor, + appcoinsRewards: AppcoinsRewards, billing: Billing, + sharedPreferences: SharedPreferences, + packageManager: PackageManager, + backupInteract: BackupInteractContract): InAppPurchaseInteractor { + return InAppPurchaseInteractor(asfInAppPurchaseInteractor, bdsInAppPurchaseInteractor, + ExternalBillingSerializer(), appcoinsRewards, billing, sharedPreferences, packageManager, + backupInteract) + } + + @Provides + fun provideLocalPaymentInteractor(repository: InAppDeepLinkRepository, + walletService: WalletService, + partnerAddressService: AddressService, + inAppPurchaseInteractor: InAppPurchaseInteractor, + billing: Billing, billingMessagesMapper: BillingMessagesMapper, + supportInteractor: SupportInteractor, + walletBlockedInteract: WalletBlockedInteract, + smsValidationInteract: SmsValidationInteract, + remoteRepository: RemoteRepository): LocalPaymentInteractor { + return LocalPaymentInteractor(repository, walletService, partnerAddressService, + inAppPurchaseInteractor, billing, billingMessagesMapper, supportInteractor, + walletBlockedInteract, smsValidationInteract, remoteRepository) + } + + @Provides + fun provideFetchCreditsInteract(balanceGetter: BalanceGetter) = + FetchCreditsInteract(balanceGetter) + + @Provides + fun provideFindDefaultNetworkInteract(networkInfo: NetworkInfo) = + FindDefaultNetworkInteract(networkInfo, AndroidSchedulers.mainThread()) + + @Singleton + @Provides + fun provideTransferInteractor(rewardsManager: RewardsManager, + balance: GetDefaultWalletBalanceInteract, + findWallet: FindDefaultWalletInteract) = + TransferInteractor(rewardsManager, TransactionDataValidator(), balance, findWallet) + + @Singleton + @Provides + fun provideAirdropInteractor(pendingTransactionService: PendingTransactionService, + airdropService: AirdropService, + findDefaultWalletInteract: FindDefaultWalletInteract, + airdropChainIdMapper: AirdropChainIdMapper): AirdropInteractor { + return AirdropInteractor( + Airdrop(AppcoinsTransactionService(pendingTransactionService), BehaviorSubject.create(), + airdropService), findDefaultWalletInteract, airdropChainIdMapper) + } + + @Provides + fun provideAdyenPaymentInteractor(context: Context, + adyenPaymentRepository: AdyenPaymentRepository, + inAppPurchaseInteractor: InAppPurchaseInteractor, + partnerAddressService: AddressService, billing: Billing, + walletService: WalletService, + supportInteractor: SupportInteractor, + walletBlockedInteract: WalletBlockedInteract, + smsValidationInteract: SmsValidationInteract): AdyenPaymentInteractor { + return AdyenPaymentInteractor(adyenPaymentRepository, inAppPurchaseInteractor, + inAppPurchaseInteractor.billingMessagesMapper, partnerAddressService, billing, + walletService, supportInteractor, walletBlockedInteract, smsValidationInteract) + } + + @Provides + fun provideWalletCreatorInteract(accountRepository: WalletRepositoryType, + passwordStore: PasswordStore, syncScheduler: ExecutorScheduler) = + WalletCreatorInteract(accountRepository, passwordStore, syncScheduler) + + @Provides + fun provideOnboardingInteract(walletService: WalletService, + preferencesRepositoryType: PreferencesRepositoryType, + supportInteractor: SupportInteractor, gamification: Gamification, + smsValidationInteract: SmsValidationInteract, + referralInteractor: ReferralInteractorContract, + bdsRepository: BdsRepository) = + OnboardingInteract(walletService, preferencesRepositoryType, supportInteractor, gamification, + smsValidationInteract, referralInteractor, bdsRepository) + + @Provides + fun provideGamificationInteractor(gamification: Gamification, + defaultWallet: FindDefaultWalletInteract, + conversionService: LocalCurrencyConversionService) = + GamificationInteractor(gamification, defaultWallet, conversionService) + + @Provides + fun providePromotionsInteractor(referralInteractor: ReferralInteractorContract, + gamificationInteractor: GamificationInteractor, + promotionsRepository: PromotionsRepository, + findDefaultWalletInteract: FindDefaultWalletInteract, + gamificationMapper: GamificationMapper): PromotionsInteractorContract { + return PromotionsInteractor(referralInteractor, gamificationInteractor, + promotionsRepository, findDefaultWalletInteract, gamificationMapper) + } + + @Provides + fun provideReferralInteractor(preferences: SharedPreferences, + findDefaultWalletInteract: FindDefaultWalletInteract, + promotionsRepository: PromotionsRepository): ReferralInteractorContract { + return ReferralInteractor(SharedPreferencesReferralLocalData(preferences), + findDefaultWalletInteract, promotionsRepository) + } + + @Provides + fun providesShareLinkInteractor(repository: ShareLinkRepository, + interactor: FindDefaultWalletInteract, + inAppPurchaseInteractor: InAppPurchaseInteractor) = + ShareLinkInteractor(repository, interactor, inAppPurchaseInteractor) + + @Singleton + @Provides + fun providesTopUpInteractor(repository: BdsRepository, + conversionService: LocalCurrencyConversionService, + gamificationInteractor: GamificationInteractor, + topUpValuesService: TopUpValuesService, + walletBlockedInteract: WalletBlockedInteract, + inAppPurchaseInteractor: InAppPurchaseInteractor, + supportInteractor: SupportInteractor) = + TopUpInteractor(repository, conversionService, gamificationInteractor, topUpValuesService, + LinkedHashMap(), TopUpLimitValues(), walletBlockedInteract, inAppPurchaseInteractor, + supportInteractor) + + @Singleton + @Provides + fun provideSmsValidationInteract(smsValidationRepository: SmsValidationRepositoryType, + preferencesRepositoryType: PreferencesRepositoryType) = + SmsValidationInteract(smsValidationRepository, preferencesRepositoryType) + + @Singleton + @Provides + fun provideBalanceInteract(findDefaultWalletInteract: FindDefaultWalletInteract, + balanceRepository: BalanceRepository, + preferencesRepositoryType: PreferencesRepositoryType, + smsValidationInteract: SmsValidationInteract) = + BalanceInteract(findDefaultWalletInteract, balanceRepository, + preferencesRepositoryType, smsValidationInteract) + + @Provides + fun provideAutoUpdateInteract(autoUpdateRepository: AutoUpdateRepository, + @Named("local_version_code") + localVersionCode: Int, packageManager: PackageManager, + sharedPreferences: PreferencesRepositoryType, + context: Context) = + AutoUpdateInteract(autoUpdateRepository, localVersionCode, Build.VERSION.SDK_INT, + packageManager, context.packageName, sharedPreferences) + + @Singleton + @Provides + fun provideFileInteract(context: Context, contentResolver: ContentResolver, + preferencesRepositoryType: PreferencesRepositoryType) = + FileInteractor(context, contentResolver, preferencesRepositoryType) + + @Provides + fun providePaymentMethodsInteractor(walletService: WalletService, + supportInteractor: SupportInteractor, + gamificationInteractor: GamificationInteractor, + balanceInteract: BalanceInteract, + walletBlockedInteract: WalletBlockedInteract, + inAppPurchaseInteractor: InAppPurchaseInteractor, + preferencesRepositoryType: PreferencesRepositoryType, + billing: Billing, + bdsPendingTransactionService: BdsPendingTransactionService): PaymentMethodsInteractor { + return PaymentMethodsInteractor(walletService, supportInteractor, gamificationInteractor, + balanceInteract, walletBlockedInteract, inAppPurchaseInteractor, preferencesRepositoryType, + billing, bdsPendingTransactionService) + } + + @Provides + fun provideMergedAppcoinsInteractor(balanceInteract: BalanceInteract, + walletBlockedInteract: WalletBlockedInteract, + supportInteractor: SupportInteractor, + inAppPurchaseInteractor: InAppPurchaseInteractor, + walletService: WalletService, + preferencesRepositoryType: PreferencesRepositoryType): MergedAppcoinsInteractor { + return MergedAppcoinsInteractor(balanceInteract, walletBlockedInteract, supportInteractor, + inAppPurchaseInteractor, walletService, preferencesRepositoryType) + } + + @Provides + fun providesAppcoinsRewardsBuyInteract(inAppPurchaseInteractor: InAppPurchaseInteractor, + supportInteractor: SupportInteractor, + walletService: WalletService, + walletBlockedInteract: WalletBlockedInteract, + smsValidationInteract: SmsValidationInteract): AppcoinsRewardsBuyInteract { + return AppcoinsRewardsBuyInteract(inAppPurchaseInteractor, supportInteractor, walletService, + walletBlockedInteract, smsValidationInteract) + } + + @Provides + fun providesOnChainBuyInteract(inAppPurchaseInteractor: InAppPurchaseInteractor, + supportInteractor: SupportInteractor, + walletService: WalletService, + walletBlockedInteract: WalletBlockedInteract, + smsValidationInteract: SmsValidationInteract): OnChainBuyInteract { + return OnChainBuyInteract(inAppPurchaseInteractor, supportInteractor, walletService, + walletBlockedInteract, smsValidationInteract) + } + + @Singleton + @Provides + fun providesPermissionsInteractor(permissions: Permissions, + walletService: FindDefaultWalletInteract): PermissionsInteractor { + return PermissionsInteractor(permissions, walletService) + } + + @Singleton + @Provides + fun provideCampaignInteract(campaignService: CampaignService, walletService: WalletService, + autoUpdateInteract: AutoUpdateInteract, + findDefaultWalletInteract: FindDefaultWalletInteract, + sharedPreferences: PreferencesRepositoryType): CampaignInteract { + return CampaignInteract(campaignService, walletService, autoUpdateInteract, + AdvertisingThrowableCodeMapper(), findDefaultWalletInteract, sharedPreferences) + } + + @Singleton + @Provides + fun provideSupportInteractor(preferences: SupportSharedPreferences, app: App): SupportInteractor { + return SupportInteractor(preferences, app) + } + + @Provides + fun provideTransactionsViewInteract(findDefaultNetworkInteract: FindDefaultNetworkInteract, + findDefaultWalletInteract: FindDefaultWalletInteract, + fetchTransactionsInteract: FetchTransactionsInteract, + gamificationInteractor: GamificationInteractor, + balanceInteract: BalanceInteract, + promotionsInteractorContract: PromotionsInteractorContract, + cardNotificationsInteractor: CardNotificationsInteractor, + autoUpdateInteract: AutoUpdateInteract): TransactionViewInteract { + return TransactionViewInteract(findDefaultNetworkInteract, findDefaultWalletInteract, + fetchTransactionsInteract, gamificationInteractor, balanceInteract, + promotionsInteractorContract, cardNotificationsInteractor, autoUpdateInteract) + } + + @Provides + fun provideFetchTransactionsInteract( + transactionRepository: TransactionRepositoryType): FetchTransactionsInteract { + return FetchTransactionsInteract(transactionRepository) + } + + @Provides + fun provideBackupInteractor(sharedPreferences: PreferencesRepositoryType, + gamificationInteractor: GamificationInteractor, + fetchTransactionsInteract: FetchTransactionsInteract, + balanceInteract: BalanceInteract, + findDefaultWalletInteract: FindDefaultWalletInteract): BackupInteractContract { + return BackupInteract(sharedPreferences, fetchTransactionsInteract, balanceInteract, + gamificationInteractor, findDefaultWalletInteract) + } + + @Provides + fun provideCardNotificationInteractor(referralInteractor: ReferralInteractorContract, + autoUpdateInteract: AutoUpdateInteract, + backupInteract: BackupInteractContract, + promotionsInteractorContract: PromotionsInteractorContract): CardNotificationsInteractor { + return CardNotificationsInteractor(referralInteractor, autoUpdateInteract, + backupInteract, promotionsInteractorContract) + } + + @Singleton + @Provides + fun provideTokenRepository(defaultTokenProvider: DefaultTokenProvider, + walletRepositoryType: WalletRepositoryType): TokenRepository { + return TokenRepository(defaultTokenProvider, walletRepositoryType) + } + + @Provides + fun provideSetDefaultAccountInteract( + accountRepository: WalletRepositoryType): SetDefaultWalletInteract { + return SetDefaultWalletInteract(accountRepository) + } + + @Provides + fun provideDeleteAccountInteract(accountRepository: WalletRepositoryType, store: PasswordStore, + preferencesRepositoryType: PreferencesRepositoryType): DeleteWalletInteract { + return DeleteWalletInteract(accountRepository, store, preferencesRepositoryType) + } + + @Provides + fun provideFetchAccountsInteract(accountRepository: WalletRepositoryType): FetchWalletsInteract { + return FetchWalletsInteract(accountRepository) + } + + @Provides + fun provideExportWalletInteract(walletRepository: WalletRepositoryType, + passwordStore: PasswordStore): ExportWalletInteract { + return ExportWalletInteract(walletRepository, passwordStore) + } + + @Provides + fun provideWalletsInteract(balanceInteract: BalanceInteract, + fetchWalletsInteract: FetchWalletsInteract, + walletCreatorInteract: WalletCreatorInteract, + supportInteractor: SupportInteractor, + sharedPreferencesRepository: SharedPreferencesRepository, + gamification: Gamification, logger: Logger): WalletsInteract { + return WalletsInteract(balanceInteract, fetchWalletsInteract, walletCreatorInteract, + supportInteractor, sharedPreferencesRepository, gamification, logger) + } + + @Provides + fun provideWalletDetailInteract(balanceInteract: BalanceInteract, + setDefaultWalletInteract: SetDefaultWalletInteract, + supportInteractor: SupportInteractor, + gamification: Gamification): WalletDetailsInteractor { + return WalletDetailsInteractor(balanceInteract, setDefaultWalletInteract, supportInteractor, + gamification) + } + + @Singleton + @Provides + fun provideRestoreWalletInteract( + walletRepository: WalletRepositoryType, passwordStore: PasswordStore, + preferencesRepositoryType: PreferencesRepositoryType, + setDefaultWalletInteract: SetDefaultWalletInteract, + fileInteractor: FileInteractor): RestoreWalletInteractor { + return RestoreWalletInteractor(walletRepository, setDefaultWalletInteract, + passwordStore, preferencesRepositoryType, fileInteractor) + } + + @Singleton + @Provides + fun provideRestoreWalletInteractor(gson: Gson, balanceInteract: BalanceInteract, + restoreWalletInteractor: RestoreWalletInteractor): RestoreWalletPasswordInteractor { + return RestoreWalletPasswordInteractor(gson, balanceInteract, restoreWalletInteractor) + } + + @Provides + fun providesSettingsInteract(findDefaultWalletInteract: FindDefaultWalletInteract, + supportInteractor: SupportInteractor, + walletsInteract: WalletsInteract, + autoUpdateInteract: AutoUpdateInteract, + fingerPrintInteractor: FingerPrintInteractor, + walletsEventSender: WalletsEventSender, + preferencesRepositoryType: PreferencesRepositoryType): SettingsInteractor { + return SettingsInteractor(findDefaultWalletInteract, supportInteractor, walletsInteract, + autoUpdateInteract, fingerPrintInteractor, walletsEventSender, preferencesRepositoryType) + } + + @Provides + fun provideIabInteract(inAppPurchaseInteractor: InAppPurchaseInteractor, + autoUpdateInteract: AutoUpdateInteract, + supportInteractor: SupportInteractor, + gamificationRepository: Gamification, + walletBlockedInteract: WalletBlockedInteract): IabInteract { + return IabInteract(inAppPurchaseInteractor, autoUpdateInteract, supportInteractor, + gamificationRepository, walletBlockedInteract) + } + + @Provides + fun provideFingerprintInteract(biometricManager: BiometricManager, + packageManager: PackageManager, + preferencesRepositoryType: PreferencesRepositoryType): FingerPrintInteractor { + return FingerPrintInteractor(biometricManager, packageManager, preferencesRepositoryType) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/MyAddressModule.kt b/app/src/main/java/com/asfoundation/wallet/di/MyAddressModule.kt new file mode 100644 index 00000000000..1a56c2de79f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/MyAddressModule.kt @@ -0,0 +1,16 @@ +package com.asfoundation.wallet.di + +import com.asfoundation.wallet.router.TransactionsRouter +import com.asfoundation.wallet.viewmodel.MyAddressViewModelFactory +import dagger.Module +import dagger.Provides + +@Module +class MyAddressModule { + @Provides + fun providesMyAddressViewModelFactory(transactionsRouter: TransactionsRouter) = + MyAddressViewModelFactory(transactionsRouter) + + @Provides + fun provideTransactionsRouter() = TransactionsRouter() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/OperationSources.java b/app/src/main/java/com/asfoundation/wallet/di/OperationSources.java index 6c1f8a98bd4..73e6d351347 100644 --- a/app/src/main/java/com/asfoundation/wallet/di/OperationSources.java +++ b/app/src/main/java/com/asfoundation/wallet/di/OperationSources.java @@ -2,9 +2,9 @@ import com.asfoundation.wallet.poa.ProofOfAttentionService; import com.asfoundation.wallet.poa.ProofStatus; -import com.asfoundation.wallet.repository.PaymentTransaction; import com.asfoundation.wallet.ui.iab.AppcoinsOperationsDataSaver; import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor; +import com.asfoundation.wallet.ui.iab.Payment; import io.reactivex.Observable; import io.reactivex.schedulers.Schedulers; import java.util.ArrayList; @@ -26,8 +26,8 @@ public List getSources() { list.add(() -> inAppPurchaseInteractor.getAll() .subscribeOn(Schedulers.io()) .flatMap(paymentTransactions -> Observable.fromIterable(paymentTransactions) - .filter(paymentTransaction -> paymentTransaction.getState() - .equals(PaymentTransaction.PaymentState.COMPLETED)) + .filter(paymentTransaction -> paymentTransaction.getStatus() + .equals(Payment.Status.COMPLETED)) .map( paymentTransaction -> new AppcoinsOperationsDataSaver.OperationDataSource .OperationData( diff --git a/app/src/main/java/com/asfoundation/wallet/di/RepositoriesModule.java b/app/src/main/java/com/asfoundation/wallet/di/RepositoriesModule.java deleted file mode 100644 index 4fdb6dc9e21..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/RepositoriesModule.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.asfoundation.wallet.di; - -import android.content.Context; -import com.asfoundation.wallet.interact.DefaultTokenProvider; -import com.asfoundation.wallet.poa.BlockchainErrorMapper; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; -import com.asfoundation.wallet.repository.NonceGetter; -import com.asfoundation.wallet.repository.PendingTransactionService; -import com.asfoundation.wallet.repository.PreferenceRepositoryType; -import com.asfoundation.wallet.repository.TokenLocalSource; -import com.asfoundation.wallet.repository.TokenRepository; -import com.asfoundation.wallet.repository.TokenRepositoryType; -import com.asfoundation.wallet.repository.TokensRealmSource; -import com.asfoundation.wallet.repository.TransactionLocalSource; -import com.asfoundation.wallet.repository.TransactionRepository; -import com.asfoundation.wallet.repository.TransactionRepositoryType; -import com.asfoundation.wallet.repository.TransactionsRealmCache; -import com.asfoundation.wallet.repository.WalletRepository; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import com.asfoundation.wallet.repository.Web3jProvider; -import com.asfoundation.wallet.repository.Web3jService; -import com.asfoundation.wallet.service.AccountKeystoreService; -import com.asfoundation.wallet.service.EthplorerTokenService; -import com.asfoundation.wallet.service.GethKeystoreAccountService; -import com.asfoundation.wallet.service.RealmManager; -import com.asfoundation.wallet.service.TickerService; -import com.asfoundation.wallet.service.TokenExplorerClientType; -import com.asfoundation.wallet.service.TransactionsNetworkClient; -import com.asfoundation.wallet.service.TransactionsNetworkClientType; -import com.google.gson.Gson; -import dagger.Module; -import dagger.Provides; -import io.reactivex.schedulers.Schedulers; -import java.io.File; -import javax.inject.Singleton; -import okhttp3.OkHttpClient; - -@Module public class RepositoriesModule { - - @Singleton @Provides AccountKeystoreService provideAccountKeyStoreService(Context context) { - File file = new File(context.getFilesDir(), "keystore/keystore"); - return new GethKeystoreAccountService(file); - } - - @Singleton @Provides WalletRepositoryType provideWalletRepository(OkHttpClient okHttpClient, - PreferenceRepositoryType preferenceRepositoryType, - AccountKeystoreService accountKeystoreService, - EthereumNetworkRepositoryType networkRepository) { - return new WalletRepository(okHttpClient, preferenceRepositoryType, accountKeystoreService, - networkRepository); - } - - @Singleton @Provides Web3jService providesWeb3jService(Web3jProvider web3jProvider) { - return new Web3jService(web3jProvider); - } - - @Singleton @Provides Web3jProvider providesWeb3jProvider( - EthereumNetworkRepositoryType ethereumNetworkRepository, OkHttpClient client) { - return new Web3jProvider(ethereumNetworkRepository, client); - } - - @Singleton @Provides PendingTransactionService providesPendingTransactionService( - Web3jService web3jService) { - return new PendingTransactionService(web3jService, Schedulers.computation(), 5); - } - - @Singleton @Provides TransactionRepositoryType provideTransactionRepository( - EthereumNetworkRepositoryType networkRepository, - AccountKeystoreService accountKeystoreService, - TransactionsNetworkClientType blockExplorerClient, TransactionLocalSource inDiskCache, - DefaultTokenProvider defaultTokenProvider, NonceGetter nonceGetter) { - return new TransactionRepository(networkRepository, accountKeystoreService, inDiskCache, - blockExplorerClient, defaultTokenProvider, nonceGetter, new BlockchainErrorMapper()); - } - - @Singleton @Provides TransactionLocalSource provideTransactionInDiskCache( - RealmManager realmManager) { - return new TransactionsRealmCache(realmManager); - } - - @Singleton @Provides TransactionsNetworkClientType provideBlockExplorerClient( - OkHttpClient httpClient, Gson gson, EthereumNetworkRepositoryType ethereumNetworkRepository) { - return new TransactionsNetworkClient(httpClient, gson, ethereumNetworkRepository); - } - - @Singleton @Provides TokenRepositoryType provideTokenRepository(OkHttpClient okHttpClient, - EthereumNetworkRepositoryType ethereumNetworkRepository, - WalletRepositoryType walletRepository, TokenExplorerClientType tokenExplorerClientType, - TokenLocalSource tokenLocalSource, TransactionLocalSource inDiskCache, - TickerService tickerService) { - return new TokenRepository(okHttpClient, ethereumNetworkRepository, walletRepository, - tokenExplorerClientType, tokenLocalSource, inDiskCache, tickerService); - } - - @Singleton @Provides TokenExplorerClientType provideTokenService(OkHttpClient okHttpClient, - Gson gson) { - return new EthplorerTokenService(okHttpClient, gson); - } - - @Singleton @Provides TokenLocalSource provideRealmTokenSource(RealmManager realmManager) { - return new TokensRealmSource(realmManager); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/RepositoryModule.kt b/app/src/main/java/com/asfoundation/wallet/di/RepositoryModule.kt new file mode 100644 index 00000000000..97d8fcebf9a --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/RepositoryModule.kt @@ -0,0 +1,242 @@ +package com.asfoundation.wallet.di + +import android.content.Context +import android.content.SharedPreferences +import androidx.room.Room +import com.appcoins.wallet.bdsbilling.repository.BdsApiResponseMapper +import com.appcoins.wallet.bdsbilling.repository.BdsApiSecondary +import com.appcoins.wallet.bdsbilling.repository.BdsRepository +import com.appcoins.wallet.bdsbilling.repository.RemoteRepository +import com.appcoins.wallet.bdsbilling.repository.RemoteRepository.BdsApi +import com.appcoins.wallet.billing.adyen.AdyenPaymentRepository +import com.appcoins.wallet.billing.adyen.AdyenPaymentRepository.AdyenApi +import com.appcoins.wallet.billing.adyen.AdyenResponseMapper +import com.appcoins.wallet.gamification.repository.* +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.analytics.AmplitudeAnalytics +import com.asfoundation.wallet.analytics.RakamAnalytics +import com.asfoundation.wallet.billing.partners.InstallerService +import com.asfoundation.wallet.billing.purchase.InAppDeepLinkRepository +import com.asfoundation.wallet.billing.purchase.LocalPaymentsLinkRepository +import com.asfoundation.wallet.billing.purchase.LocalPaymentsLinkRepository.DeepLinkApi +import com.asfoundation.wallet.billing.share.BdsShareLinkRepository +import com.asfoundation.wallet.billing.share.BdsShareLinkRepository.BdsShareLinkApi +import com.asfoundation.wallet.billing.share.ShareLinkRepository +import com.asfoundation.wallet.entity.NetworkInfo +import com.asfoundation.wallet.identification.IdsRepository +import com.asfoundation.wallet.interact.DefaultTokenProvider +import com.asfoundation.wallet.interact.GetDefaultWalletBalanceInteract +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.poa.BlockchainErrorMapper +import com.asfoundation.wallet.repository.* +import com.asfoundation.wallet.repository.OffChainTransactionsRepository.TransactionsApi +import com.asfoundation.wallet.repository.TransactionsDatabase.Companion.MIGRATION_1_2 +import com.asfoundation.wallet.repository.TransactionsDatabase.Companion.MIGRATION_2_3 +import com.asfoundation.wallet.repository.TransactionsDatabase.Companion.MIGRATION_3_4 +import com.asfoundation.wallet.service.* +import com.asfoundation.wallet.ui.balance.AppcoinsBalanceRepository +import com.asfoundation.wallet.ui.balance.BalanceRepository +import com.asfoundation.wallet.ui.balance.database.BalanceDetailsDatabase +import com.asfoundation.wallet.ui.balance.database.BalanceDetailsMapper +import com.asfoundation.wallet.ui.gamification.SharedPreferencesGamificationLocalData +import com.asfoundation.wallet.ui.iab.AppCoinsOperationMapper +import com.asfoundation.wallet.ui.iab.AppCoinsOperationRepository +import com.asfoundation.wallet.ui.iab.database.AppCoinsOperationDatabase +import com.asfoundation.wallet.ui.iab.raiden.MultiWalletNonceObtainer +import com.asfoundation.wallet.wallet_blocked.WalletStatusApi +import com.asfoundation.wallet.wallet_blocked.WalletStatusRepository +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.gson.Gson +import dagger.Module +import dagger.Provides +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.jackson.JacksonConverterFactory +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +import javax.inject.Named +import javax.inject.Singleton + +@Module +class RepositoryModule { + + @Singleton + @Provides + fun providePreferencesRepository( + sharedPreferences: SharedPreferences): SharedPreferencesRepository { + return SharedPreferencesRepository(sharedPreferences) + } + + @Singleton + @Provides + fun providePreferenceRepositoryType( + sharedPreferenceRepository: SharedPreferencesRepository): PreferencesRepositoryType { + return sharedPreferenceRepository + } + + @Singleton + @Provides + fun provideGasSettingsRepository(gasService: GasService): GasSettingsRepositoryType = + GasSettingsRepository(gasService) + + @Singleton + @Provides + fun providesAppCoinsOperationRepository(context: Context): AppCoinsOperationRepository { + return AppCoinsOperationRepository( + Room.databaseBuilder(context.applicationContext, AppCoinsOperationDatabase::class.java, + "appcoins_operations_data") + .build() + .appCoinsOperationDao(), AppCoinsOperationMapper()) + } + + @Singleton + @Provides + fun provideRemoteRepository(bdsApi: BdsApi, api: BdsApiSecondary): RemoteRepository { + return RemoteRepository(bdsApi, BdsApiResponseMapper(), api) + } + + @Singleton + @Provides + fun provideBdsRepository(repository: RemoteRepository) = BdsRepository(repository) + + @Singleton + @Provides + fun provideAdyenPaymentRepository( + @Named("default") client: OkHttpClient): AdyenPaymentRepository { + val api = Retrofit.Builder() + .baseUrl(BuildConfig.BASE_HOST + "/broker/8.20200815/gateways/adyen_v2/") + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(AdyenApi::class.java) + return AdyenPaymentRepository(api, AdyenResponseMapper()) + } + + @Provides + fun providePromotionsRepository(api: GamificationApi, preferences: SharedPreferences, + promotionDao: PromotionDao, levelsDao: LevelsDao, + levelDao: LevelDao): PromotionsRepository { + return BdsPromotionsRepository(api, + SharedPreferencesGamificationLocalData(preferences, promotionDao, levelsDao, levelDao)) + } + + @Singleton + @Provides + fun providesShareLinkRepository(api: BdsShareLinkApi): ShareLinkRepository { + return BdsShareLinkRepository(api) + } + + @Provides + fun providesOffChainTransactionsRepository( + @Named("blockchain") client: OkHttpClient): OffChainTransactionsRepository { + val objectMapper = ObjectMapper() + val df: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US) + objectMapper.dateFormat = df + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + val retrofit = Retrofit.Builder() + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(JacksonConverterFactory.create(objectMapper)) + .client(client) + .baseUrl(BuildConfig.BACKEND_HOST) + .build() + return OffChainTransactionsRepository(retrofit.create(TransactionsApi::class.java), + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US)) + } + + @Singleton + @Provides + fun provideTransactionRepository(networkInfo: NetworkInfo, + accountKeystoreService: AccountKeystoreService, + defaultTokenProvider: DefaultTokenProvider, + nonceObtainer: MultiWalletNonceObtainer, + transactionsNetworkRepository: OffChainTransactions, + context: Context, + sharedPreferences: SharedPreferences): TransactionRepositoryType { + + val transactionsDao = Room.databaseBuilder(context.applicationContext, + TransactionsDatabase::class.java, + "transactions_database") + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) + .build() + .transactionsDao() + val localRepository: TransactionsRepository = + TransactionsLocalRepository(transactionsDao, sharedPreferences) + return BackendTransactionRepository(networkInfo, accountKeystoreService, defaultTokenProvider, + BlockchainErrorMapper(), nonceObtainer, Schedulers.io(), + transactionsNetworkRepository, localRepository, TransactionMapper(), + CompositeDisposable(), Schedulers.io()) + } + + @Singleton + @Provides + fun provideBalanceRepository(context: Context, + localCurrencyConversionService: LocalCurrencyConversionService, + getDefaultWalletBalanceInteract: GetDefaultWalletBalanceInteract): BalanceRepository { + return AppcoinsBalanceRepository(getDefaultWalletBalanceInteract, + localCurrencyConversionService, + Room.databaseBuilder(context.applicationContext, + BalanceDetailsDatabase::class.java, + "balance_details") + .build() + .balanceDetailsDao(), BalanceDetailsMapper(), Schedulers.io()) + } + + @Provides + fun provideAutoUpdateRepository(autoUpdateService: AutoUpdateService) = + AutoUpdateRepository(autoUpdateService) + + @Singleton + @Provides + fun provideIdsRepository(context: Context, + sharedPreferencesRepository: SharedPreferencesRepository, + installerService: InstallerService): IdsRepository { + return IdsRepository(context.contentResolver, sharedPreferencesRepository, installerService) + } + + @Singleton + @Provides + fun provideWalletRepository(preferencesRepositoryType: PreferencesRepositoryType, + accountKeystoreService: AccountKeystoreService, + walletBalanceService: WalletBalanceService, + analyticsSetup: RakamAnalytics, + amplitudeAnalytics: AmplitudeAnalytics): WalletRepositoryType { + return WalletRepository(preferencesRepositoryType, accountKeystoreService, + walletBalanceService, Schedulers.io(), analyticsSetup, amplitudeAnalytics) + } + + @Singleton + @Provides + fun provideTokenRepository( + defaultTokenProvider: DefaultTokenProvider, + walletRepositoryType: WalletRepositoryType): TokenRepositoryType { + return TokenRepository(defaultTokenProvider, walletRepositoryType) + } + + @Singleton + @Provides + fun provideSmsValidationRepository( + smsValidationApi: SmsValidationApi, gson: Gson, + logger: Logger): SmsValidationRepositoryType { + return SmsValidationRepository(smsValidationApi, gson, logger) + } + + @Singleton + @Provides + fun provideWalletStatusRepository( + walletStatusApi: WalletStatusApi): WalletStatusRepository { + return WalletStatusRepository(walletStatusApi) + } + + @Singleton + @Provides + fun providesDeepLinkRepository(api: DeepLinkApi): InAppDeepLinkRepository { + return LocalPaymentsLinkRepository(api) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/SendModule.java b/app/src/main/java/com/asfoundation/wallet/di/SendModule.java deleted file mode 100644 index e216f101cb7..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/SendModule.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.interact.FetchGasSettingsInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.router.ConfirmationRouter; -import com.asfoundation.wallet.util.TransferParser; -import com.asfoundation.wallet.viewmodel.SendViewModelFactory; -import dagger.Module; -import dagger.Provides; -import io.reactivex.subjects.PublishSubject; - -@Module public class SendModule { - @Provides SendViewModelFactory provideSendViewModelFactory( - FindDefaultWalletInteract findDefaultWalletInteract, ConfirmationRouter confirmationRouter, - FetchGasSettingsInteract fetchGasSettingsInteract, TransferParser transferParser) { - return new SendViewModelFactory(findDefaultWalletInteract, fetchGasSettingsInteract, - confirmationRouter, transferParser); - } - - @Provides ConfirmationRouter provideConfirmationRouter() { - return new ConfirmationRouter(PublishSubject.create()); - } - -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/SendModule.kt b/app/src/main/java/com/asfoundation/wallet/di/SendModule.kt new file mode 100644 index 00000000000..9304d1e7a80 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/SendModule.kt @@ -0,0 +1,30 @@ +package com.asfoundation.wallet.di + +import com.asfoundation.wallet.interact.FetchGasSettingsInteract +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.router.ConfirmationRouter +import com.asfoundation.wallet.router.TransactionsRouter +import com.asfoundation.wallet.util.TransferParser +import com.asfoundation.wallet.viewmodel.SendViewModelFactory +import dagger.Module +import dagger.Provides +import io.reactivex.subjects.PublishSubject + +@Module +class SendModule { + @Provides + fun provideSendViewModelFactory(findDefaultWalletInteract: FindDefaultWalletInteract, + confirmationRouter: ConfirmationRouter, + fetchGasSettingsInteract: FetchGasSettingsInteract, + transferParser: TransferParser, + transactionsRouter: TransactionsRouter): SendViewModelFactory { + return SendViewModelFactory(findDefaultWalletInteract, fetchGasSettingsInteract, + confirmationRouter, transferParser, transactionsRouter) + } + + @Provides + fun provideConfirmationRouter() = ConfirmationRouter(PublishSubject.create()) + + @Provides + fun provideTransactionsRouter() = TransactionsRouter() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/ServiceBuilders.kt b/app/src/main/java/com/asfoundation/wallet/di/ServiceBuilders.kt new file mode 100644 index 00000000000..20ea8a96e5e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/ServiceBuilders.kt @@ -0,0 +1,21 @@ +package com.asfoundation.wallet.di + +import com.asfoundation.wallet.advertise.AdvertisingService +import com.asfoundation.wallet.advertise.WalletPoAService +import com.asfoundation.wallet.transactions.PerkBonusService +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class ServiceBuilders { + + @ContributesAndroidInjector + abstract fun bindWalletPoAService(): WalletPoAService + + @ContributesAndroidInjector + abstract fun bindPerkBonusService(): PerkBonusService + + @ActivityScope + @ContributesAndroidInjector + abstract fun bindAdvertisingService(): AdvertisingService +} diff --git a/app/src/main/java/com/asfoundation/wallet/di/ServiceModule.kt b/app/src/main/java/com/asfoundation/wallet/di/ServiceModule.kt new file mode 100644 index 00000000000..14c3cb6aed6 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/ServiceModule.kt @@ -0,0 +1,547 @@ +package com.asfoundation.wallet.di + +import android.content.Context +import android.os.Build +import com.appcoins.wallet.appcoins.rewards.repository.backend.BackendApi +import com.appcoins.wallet.bdsbilling.Billing +import com.appcoins.wallet.bdsbilling.BillingPaymentProofSubmission +import com.appcoins.wallet.bdsbilling.ProxyService +import com.appcoins.wallet.bdsbilling.WalletService +import com.appcoins.wallet.bdsbilling.repository.BdsApiSecondary +import com.appcoins.wallet.bdsbilling.repository.RemoteRepository.BdsApi +import com.appcoins.wallet.commons.MemoryCache +import com.appcoins.wallet.gamification.repository.GamificationApi +import com.appcoins.wallet.gamification.repository.entity.PromotionsDeserializer +import com.appcoins.wallet.gamification.repository.entity.PromotionsResponse +import com.appcoins.wallet.gamification.repository.entity.PromotionsSerializer +import com.aptoide.apk.injector.extractor.domain.IExtract +import com.asf.appcoins.sdk.contractproxy.AppCoinsAddressProxySdk +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.AirdropService +import com.asfoundation.wallet.advertise.CampaignInteract +import com.asfoundation.wallet.analytics.AnalyticsAPI +import com.asfoundation.wallet.apps.Applications +import com.asfoundation.wallet.billing.partners.* +import com.asfoundation.wallet.billing.purchase.LocalPaymentsLinkRepository.DeepLinkApi +import com.asfoundation.wallet.billing.share.BdsShareLinkRepository.BdsShareLinkApi +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.interact.DefaultTokenProvider +import com.asfoundation.wallet.interact.GetDefaultWalletBalanceInteract +import com.asfoundation.wallet.interact.SendTransactionInteract +import com.asfoundation.wallet.interact.WalletCreatorInteract +import com.asfoundation.wallet.poa.* +import com.asfoundation.wallet.repository.* +import com.asfoundation.wallet.service.* +import com.asfoundation.wallet.service.AutoUpdateService.AutoUpdateApi +import com.asfoundation.wallet.service.CampaignService.CampaignApi +import com.asfoundation.wallet.service.LocalCurrencyConversionService.TokenToLocalFiatApi +import com.asfoundation.wallet.service.TokenRateService.TokenToFiatApi +import com.asfoundation.wallet.topup.TopUpValuesApiResponseMapper +import com.asfoundation.wallet.topup.TopUpValuesService +import com.asfoundation.wallet.topup.TopUpValuesService.TopUpValuesApi +import com.asfoundation.wallet.ui.AppcoinsApps +import com.asfoundation.wallet.util.DeviceInfo +import com.asfoundation.wallet.wallet_blocked.WalletStatusApi +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import dagger.Module +import dagger.Provides +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.internal.schedulers.ExecutorScheduler +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.jackson.JacksonConverterFactory +import java.io.File +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Named +import javax.inject.Singleton + +@Module +class ServiceModule { + + @Provides + @Named("BUY_SERVICE_ON_CHAIN") + fun provideBuyServiceOnChain(sendTransactionInteract: SendTransactionInteract, + errorMapper: ErrorMapper, + @Named("wait_pending_transaction") + pendingTransactionService: TrackTransactionService, + defaultTokenProvider: DefaultTokenProvider, + countryCodeProvider: CountryCodeProvider, dataMapper: DataMapper, + addressService: AddressService): BuyService { + return BuyService(WatchedTransactionService(object : TransactionSender { + override fun send(transactionBuilder: TransactionBuilder): Single { + return sendTransactionInteract.buy(transactionBuilder) + } + }, MemoryCache(BehaviorSubject.create(), ConcurrentHashMap()), errorMapper, Schedulers.io(), + pendingTransactionService), NoValidateTransactionValidator(), defaultTokenProvider, + countryCodeProvider, dataMapper, addressService) + } + + @Provides + @Named("BUY_SERVICE_BDS") + fun provideBuyServiceBds(sendTransactionInteract: SendTransactionInteract, + errorMapper: ErrorMapper, + bdsPendingTransactionService: BdsPendingTransactionService, + billingPaymentProofSubmission: BillingPaymentProofSubmission, + defaultTokenProvider: DefaultTokenProvider, + countryCodeProvider: CountryCodeProvider, dataMapper: DataMapper, + addressService: AddressService): BuyService { + return BuyService(WatchedTransactionService(object : TransactionSender { + override fun send(transactionBuilder: TransactionBuilder): Single { + return sendTransactionInteract.buy(transactionBuilder) + } + }, MemoryCache(BehaviorSubject.create(), ConcurrentHashMap()), errorMapper, Schedulers.io(), + bdsPendingTransactionService), + BuyTransactionValidatorBds(sendTransactionInteract, billingPaymentProofSubmission, + defaultTokenProvider, addressService), defaultTokenProvider, countryCodeProvider, + dataMapper, addressService) + } + + @Provides + fun provideAllowanceService(web3jProvider: Web3jProvider, + defaultTokenProvider: DefaultTokenProvider): AllowanceService { + return AllowanceService(web3jProvider.default, defaultTokenProvider) + } + + @Singleton + @Provides + @Named("IN_APP_PURCHASE_SERVICE") + fun provideInAppPurchaseService(@Named("APPROVE_SERVICE_BDS") approveService: ApproveService, + allowanceService: AllowanceService, + @Named("BUY_SERVICE_BDS") buyService: BuyService, + balanceService: BalanceService, + errorMapper: ErrorMapper): InAppPurchaseService { + return InAppPurchaseService(MemoryCache(BehaviorSubject.create(), HashMap()), approveService, + allowanceService, buyService, balanceService, Schedulers.io(), errorMapper) + } + + @Singleton + @Provides + @Named("ASF_IN_APP_PURCHASE_SERVICE") + fun provideInAppPurchaseServiceAsf( + @Named("APPROVE_SERVICE_ON_CHAIN") approveService: ApproveService, + allowanceService: AllowanceService, @Named("BUY_SERVICE_ON_CHAIN") buyService: BuyService, + balanceService: BalanceService, errorMapper: ErrorMapper): InAppPurchaseService { + return InAppPurchaseService(MemoryCache(BehaviorSubject.create(), HashMap()), approveService, + allowanceService, buyService, balanceService, Schedulers.io(), errorMapper) + } + + @Singleton + @Provides + fun providesBdsTransactionService(billing: Billing, + billingPaymentProofSubmission: BillingPaymentProofSubmission): BdsTransactionService { + return BdsTransactionService(Schedulers.io(), MemoryCache(BehaviorSubject.create(), HashMap()), + CompositeDisposable(), + BdsPendingTransactionService(billing, Schedulers.io(), 5, billingPaymentProofSubmission)) + } + + @Singleton + @Provides + fun provideProofOfAttentionService( + hashCalculator: HashCalculator, proofWriter: ProofWriter, + disposables: TaggedCompositeDisposable, + @Named("MAX_NUMBER_PROOF_COMPONENTS") maxNumberProofComponents: Int, + countryCodeProvider: CountryCodeProvider, + addressService: AddressService, + walletService: WalletService, + campaignInteract: CampaignInteract): ProofOfAttentionService { + return ProofOfAttentionService(MemoryCache(BehaviorSubject.create(), HashMap()), + BuildConfig.APPLICATION_ID, hashCalculator, CompositeDisposable(), proofWriter, + Schedulers.computation(), maxNumberProofComponents, BackEndErrorMapper(), disposables, + countryCodeProvider, addressService, walletService, + campaignInteract) + } + + @Provides + fun provideAirdropService(@Named("blockchain") client: OkHttpClient, gson: Gson): AirdropService { + val api = Retrofit.Builder() + .baseUrl(AirdropService.BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(AirdropService.Api::class.java) + return AirdropService(api, gson, Schedulers.io()) + } + + @Singleton + @Provides + fun provideTokenRateService(@Named("blockchain") client: OkHttpClient, + objectMapper: ObjectMapper): TokenRateService { + val baseUrl = TokenRateService.CONVERSION_HOST + val api = Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(JacksonConverterFactory.create(objectMapper)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(TokenToFiatApi::class.java) + return TokenRateService(api) + } + + @Singleton + @Provides + fun provideLocalCurrencyConversionService(@Named("default") client: OkHttpClient, + objectMapper: ObjectMapper): LocalCurrencyConversionService { + val baseUrl = LocalCurrencyConversionService.CONVERSION_HOST + val api = Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(JacksonConverterFactory.create(objectMapper)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(TokenToLocalFiatApi::class.java) + return LocalCurrencyConversionService(api) + } + + @Singleton + @Provides + fun provideCurrencyConversionService(tokenRateService: TokenRateService, + localCurrencyConversionService: LocalCurrencyConversionService): CurrencyConversionService { + return CurrencyConversionService(tokenRateService, localCurrencyConversionService) + } + + @Singleton + @Provides + fun provideAccountWalletService(accountKeyService: AccountKeystoreService, + passwordStore: PasswordStore, + walletCreatorInteract: WalletCreatorInteract, + walletRepository: WalletRepositoryType, + syncScheduler: ExecutorScheduler): WalletService { + return AccountWalletService(accountKeyService, passwordStore, walletCreatorInteract, + SignDataStandardNormalizer(), walletRepository, syncScheduler) + } + + @Singleton + @Provides + fun provideProxyService(proxySdk: AppCoinsAddressProxySdk): ProxyService { + return object : ProxyService { + private val NETWORK_ID_ROPSTEN = 3 + private val NETWORK_ID_MAIN = 1 + override fun getAppCoinsAddress(debug: Boolean): Single { + return proxySdk.getAppCoinsAddress(if (debug) NETWORK_ID_ROPSTEN else NETWORK_ID_MAIN) + } + + override fun getIabAddress(debug: Boolean): Single { + return proxySdk.getIabAddress(if (debug) NETWORK_ID_ROPSTEN else NETWORK_ID_MAIN) + } + } + } + + @Singleton + @Provides + fun provideBdsPendingTransactionService( + billingPaymentProofSubmission: BillingPaymentProofSubmission, + billing: Billing): BdsPendingTransactionService { + return BdsPendingTransactionService(billing, Schedulers.io(), 5, billingPaymentProofSubmission) + } + + @Singleton + @Provides + fun providePoASubmissionService(@Named("blockchain") client: OkHttpClient): CampaignService { + val baseUrl = CampaignService.SERVICE_HOST + val api = Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(CampaignApi::class.java) + return CampaignService(api, BuildConfig.VERSION_CODE, Schedulers.io()) + } + + @Singleton + @Provides + fun providesAddressService(installerService: InstallerService, + addressService: WalletAddressService, + oemIdExtractorService: OemIdExtractorService): AddressService { + return PartnerAddressService(installerService, addressService, + DeviceInfo(Build.MANUFACTURER, Build.MODEL), oemIdExtractorService) + } + + @Singleton + @Provides + fun providesInstallerService(context: Context): InstallerService { + return InstallerSourceService(context) + } + + @Singleton + @Provides + fun providesWalletAddressService(api: BdsPartnersApi): WalletAddressService { + return PartnerWalletAddressService(api, BuildConfig.DEFAULT_STORE_ADDRESS, + BuildConfig.DEFAULT_OEM_ADDRESS) + } + + @Singleton + @Provides + fun providesTopUpValuesService(topUpValuesApi: TopUpValuesApi, + responseMapper: TopUpValuesApiResponseMapper): TopUpValuesService { + return TopUpValuesService(topUpValuesApi, responseMapper) + } + + @Singleton + @Provides + fun provideGasService(@Named("blockchain") client: OkHttpClient, gson: Gson): GasService { + return Retrofit.Builder() + .baseUrl(GasService.API_BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(GasService::class.java) + } + + @Singleton + @Provides + fun provideOemIdExtractorService(context: Context, extractor: IExtract): OemIdExtractorService { + return OemIdExtractorService(OemIdExtractorV1(context), + OemIdExtractorV2(context, extractor)) + } + + @Provides + fun provideAutoUpdateService(autoUpdateApi: AutoUpdateApi) = + AutoUpdateService(autoUpdateApi) + + @Provides + fun provideBalanceService( + getDefaultWalletBalanceInteract: GetDefaultWalletBalanceInteract): BalanceService { + return getDefaultWalletBalanceInteract + } + + @Provides + @Named("APPROVE_SERVICE_BDS") + fun provideApproveServiceBds(sendTransactionInteract: SendTransactionInteract, + errorMapper: ErrorMapper, @Named("no_wait_transaction") + noWaitPendingTransactionService: TrackTransactionService, + billingPaymentProofSubmission: BillingPaymentProofSubmission, + addressService: AddressService): ApproveService { + return ApproveService(WatchedTransactionService(object : TransactionSender { + override fun send(transactionBuilder: TransactionBuilder): Single { + return sendTransactionInteract.approve(transactionBuilder) + } + }, MemoryCache(BehaviorSubject.create(), ConcurrentHashMap()), errorMapper, Schedulers.io(), + noWaitPendingTransactionService), + ApproveTransactionValidatorBds(sendTransactionInteract, billingPaymentProofSubmission, + addressService)) + } + + @Singleton + @Provides + fun provideWalletBalanceService(@Named("default") client: OkHttpClient, + gson: Gson): WalletBalanceService { + return Retrofit.Builder() + .baseUrl(WalletBalanceService.API_BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(WalletBalanceService::class.java) + } + + @Singleton + @Provides + @Named("no_wait_transaction") + fun providesNoWaitTransactionTransactionTrackTransactionService(): TrackTransactionService { + return NotTrackTransactionService() + } + + + @Singleton + @Provides + fun providesPendingTransactionService(web3jService: Web3jService): PendingTransactionService { + return PendingTransactionService(web3jService, Schedulers.computation(), 5) + } + + @Singleton + @Provides + @Named("wait_pending_transaction") + fun providesWaitPendingTransactionTrackTransactionService( + pendingTransactionService: PendingTransactionService): TrackTransactionService { + return TrackPendingTransactionService(pendingTransactionService) + } + + @Singleton + @Provides + fun provideAccountKeyStoreService(context: Context): AccountKeystoreService { + val file = File(context.filesDir, "keystore/keystore") + return Web3jKeystoreAccountService(KeyStoreFileManager(file.absolutePath, ObjectMapper()), + ObjectMapper()) + } + + @Singleton + @Provides + fun providesWeb3jService(web3jProvider: Web3jProvider): Web3jService { + return Web3jService(web3jProvider) + } + + + @Singleton + @Provides + fun provideSmsValidationApi(@Named("default") client: OkHttpClient, + gson: Gson): SmsValidationApi { + val baseUrl = BuildConfig.BACKEND_HOST + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(SmsValidationApi::class.java) + } + + @Singleton + @Provides + fun provideWalletStatusApi(@Named("default") client: OkHttpClient, gson: Gson): WalletStatusApi { + val baseUrl = BuildConfig.BACKEND_HOST + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(WalletStatusApi::class.java) + } + + @Singleton + @Provides + fun provideDeepLinkApi(@Named("default") client: OkHttpClient, gson: Gson): DeepLinkApi { + val baseUrl = BuildConfig.CATAPPULT_BASE_HOST + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(DeepLinkApi::class.java) + } + + @Singleton + @Provides + fun providesTopUpValuesApi(@Named("default") client: OkHttpClient, gson: Gson): TopUpValuesApi { + val baseUrl = BuildConfig.CATAPPULT_BASE_HOST + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(TopUpValuesApi::class.java) + } + + @Singleton + @Provides + fun provideBdsShareLinkApi(@Named("default") client: OkHttpClient, gson: Gson): BdsShareLinkApi { + val baseUrl = BuildConfig.CATAPPULT_BASE_HOST + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(BdsShareLinkApi::class.java) + } + + @Singleton + @Provides + fun provideBdsPartnersApi(@Named("default") client: OkHttpClient, gson: Gson): BdsPartnersApi { + val baseUrl = BuildConfig.BASE_HOST + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(BdsPartnersApi::class.java) + } + + @Singleton + @Provides + fun provideAnalyticsAPI(@Named("default") client: OkHttpClient, + objectMapper: ObjectMapper): AnalyticsAPI { + return Retrofit.Builder() + .baseUrl("https://ws75.aptoide.com/api/7/") + .client(client) + .addConverterFactory(JacksonConverterFactory.create(objectMapper)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(AnalyticsAPI::class.java) + } + + @Provides + fun provideGamificationApi(@Named("default") client: OkHttpClient): GamificationApi { + val gson = GsonBuilder() + .setDateFormat("yyyy-MM-dd HH:mm") + .registerTypeAdapter(PromotionsResponse::class.java, PromotionsSerializer()) + .registerTypeAdapter(PromotionsResponse::class.java, PromotionsDeserializer()) + .create() + val baseUrl = CampaignService.SERVICE_HOST + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(GamificationApi::class.java) + } + + @Singleton + @Provides + fun provideBackendApi(@Named("default") client: OkHttpClient, gson: Gson): BackendApi { + return Retrofit.Builder() + .baseUrl(BuildConfig.BACKEND_HOST) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(BackendApi::class.java) + } + + @Singleton + @Provides + fun provideAppcoinsApps(@Named("default") client: OkHttpClient, gson: Gson): AppcoinsApps { + val appsApi = Retrofit.Builder() + .baseUrl(AppsApi.API_BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(AppsApi::class.java) + return AppcoinsApps(Applications.Builder() + .setApi(BDSAppsApi(appsApi)) + .build()) + } + + @Singleton + @Provides + fun provideBdsApi(@Named("blockchain") client: OkHttpClient, gson: Gson): BdsApi { + val baseUrl = BuildConfig.BASE_HOST + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(BdsApi::class.java) + } + + @Singleton + @Provides + fun provideBdsApiSecondary(@Named("default") client: OkHttpClient, gson: Gson): BdsApiSecondary { + val baseUrl = BuildConfig.BDS_BASE_HOST + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(BdsApiSecondary::class.java) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/SettingsFragmentModule.java b/app/src/main/java/com/asfoundation/wallet/di/SettingsFragmentModule.java deleted file mode 100644 index 7c652e56569..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/SettingsFragmentModule.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.router.ManageWalletsRouter; -import dagger.Module; -import dagger.Provides; - -@Module class SettingsFragmentModule { - @Provides ManageWalletsRouter provideManageWalletsRouter() { - return new ManageWalletsRouter(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/SettingsModule.java b/app/src/main/java/com/asfoundation/wallet/di/SettingsModule.java deleted file mode 100644 index 181fdd65bdf..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/SettingsModule.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.ui.SettingsFragment; -import dagger.Module; -import dagger.android.ContributesAndroidInjector; - -@Module public interface SettingsModule { - @FragmentScope @ContributesAndroidInjector(modules = { SettingsFragmentModule.class }) - SettingsFragment settingsFragment(); -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/SplashModule.java b/app/src/main/java/com/asfoundation/wallet/di/SplashModule.java deleted file mode 100644 index 9df509644ec..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/SplashModule.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.interact.FetchWalletsInteract; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import com.asfoundation.wallet.viewmodel.SplashViewModelFactory; -import dagger.Module; -import dagger.Provides; - -@Module public class SplashModule { - - @Provides SplashViewModelFactory provideSplashViewModelFactory( - FetchWalletsInteract fetchWalletsInteract) { - return new SplashViewModelFactory(fetchWalletsInteract); - } - - @Provides FetchWalletsInteract provideFetchWalletInteract(WalletRepositoryType walletRepository) { - return new FetchWalletsInteract(walletRepository); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/TokensModule.java b/app/src/main/java/com/asfoundation/wallet/di/TokensModule.java deleted file mode 100644 index aed585dbf66..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/TokensModule.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.interact.DefaultTokenProvider; -import com.asfoundation.wallet.interact.FetchTokensInteract; -import com.asfoundation.wallet.repository.TokenRepositoryType; -import com.asfoundation.wallet.router.AddTokenRouter; -import com.asfoundation.wallet.router.ChangeTokenCollectionRouter; -import com.asfoundation.wallet.router.SendRouter; -import com.asfoundation.wallet.router.TransactionsRouter; -import com.asfoundation.wallet.viewmodel.TokensViewModelFactory; -import dagger.Module; -import dagger.Provides; - -@Module class TokensModule { - - @Provides TokensViewModelFactory provideTokensViewModelFactory( - FetchTokensInteract fetchTokensInteract, AddTokenRouter addTokenRouter, - SendRouter sendTokenRouter, TransactionsRouter transactionsRouter, - ChangeTokenCollectionRouter changeTokenCollectionRouter) { - return new TokensViewModelFactory(fetchTokensInteract, addTokenRouter, sendTokenRouter, - transactionsRouter, changeTokenCollectionRouter); - } - - @Provides AddTokenRouter provideAddTokenRouter() { - return new AddTokenRouter(); - } - - @Provides SendRouter provideSendTokenRouter() { - return new SendRouter(); - } - - @Provides TransactionsRouter provideTransactionsRouter() { - return new TransactionsRouter(); - } - - @Provides ChangeTokenCollectionRouter provideChangeTokenCollectionRouter() { - return new ChangeTokenCollectionRouter(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/ToolsModule.java b/app/src/main/java/com/asfoundation/wallet/di/ToolsModule.java deleted file mode 100644 index 6868ea902cc..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/ToolsModule.java +++ /dev/null @@ -1,314 +0,0 @@ -package com.asfoundation.wallet.di; - -import android.arch.persistence.room.Room; -import android.content.Context; -import com.asf.wallet.BuildConfig; -import com.asfoundation.wallet.Airdrop; -import com.asfoundation.wallet.AirdropService; -import com.asfoundation.wallet.App; -import com.asfoundation.wallet.interact.AddTokenInteract; -import com.asfoundation.wallet.interact.BuildConfigDefaultTokenProvider; -import com.asfoundation.wallet.interact.DefaultTokenProvider; -import com.asfoundation.wallet.interact.FetchGasSettingsInteract; -import com.asfoundation.wallet.interact.FetchTokensInteract; -import com.asfoundation.wallet.interact.FindDefaultNetworkInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.interact.GetDefaultWalletBalance; -import com.asfoundation.wallet.interact.SendTransactionInteract; -import com.asfoundation.wallet.poa.BlockchainErrorMapper; -import com.asfoundation.wallet.poa.Calculator; -import com.asfoundation.wallet.poa.DataMapper; -import com.asfoundation.wallet.poa.HashCalculator; -import com.asfoundation.wallet.poa.ProofOfAttentionService; -import com.asfoundation.wallet.poa.ProofWriter; -import com.asfoundation.wallet.poa.TaggedCompositeDisposable; -import com.asfoundation.wallet.poa.TransactionFactory; -import com.asfoundation.wallet.repository.ApproveService; -import com.asfoundation.wallet.repository.BalanceService; -import com.asfoundation.wallet.repository.BlockChainWriter; -import com.asfoundation.wallet.repository.BuyService; -import com.asfoundation.wallet.repository.ErrorMapper; -import com.asfoundation.wallet.repository.EthereumNetworkRepository; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; -import com.asfoundation.wallet.repository.GasSettingsRepository; -import com.asfoundation.wallet.repository.GasSettingsRepositoryType; -import com.asfoundation.wallet.repository.InAppPurchaseService; -import com.asfoundation.wallet.repository.MemoryCache; -import com.asfoundation.wallet.repository.NonceGetter; -import com.asfoundation.wallet.repository.PasswordStore; -import com.asfoundation.wallet.repository.PendingTransactionService; -import com.asfoundation.wallet.repository.PreferenceRepositoryType; -import com.asfoundation.wallet.repository.SharedPreferenceRepository; -import com.asfoundation.wallet.repository.TokenRepositoryType; -import com.asfoundation.wallet.repository.TransactionRepositoryType; -import com.asfoundation.wallet.repository.TrustPasswordStore; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import com.asfoundation.wallet.repository.Web3jProvider; -import com.asfoundation.wallet.router.GasSettingsRouter; -import com.asfoundation.wallet.service.AccountKeystoreService; -import com.asfoundation.wallet.service.RealmManager; -import com.asfoundation.wallet.service.TickerService; -import com.asfoundation.wallet.service.TrustWalletTickerService; -import com.asfoundation.wallet.ui.airdrop.AirdropChainIdMapper; -import com.asfoundation.wallet.ui.airdrop.AirdropInteractor; -import com.asfoundation.wallet.ui.airdrop.AppcoinsTransactionService; -import com.asfoundation.wallet.ui.iab.AppCoinsOperationMapper; -import com.asfoundation.wallet.ui.iab.AppCoinsOperationRepository; -import com.asfoundation.wallet.ui.iab.AppInfoProvider; -import com.asfoundation.wallet.ui.iab.AppcoinsOperationsDataSaver; -import com.asfoundation.wallet.ui.iab.ImageSaver; -import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor; -import com.asfoundation.wallet.ui.iab.database.AppCoinsOperationDatabase; -import com.asfoundation.wallet.util.LogInterceptor; -import com.asfoundation.wallet.util.TransferParser; -import com.google.gson.Gson; -import dagger.Module; -import dagger.Provides; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import io.reactivex.subjects.BehaviorSubject; -import java.math.BigDecimal; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.TimeUnit; -import javax.inject.Named; -import javax.inject.Singleton; -import okhttp3.OkHttpClient; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.converter.gson.GsonConverterFactory; - -import static com.asfoundation.wallet.AirdropService.BASE_URL; - -@Module class ToolsModule { - @Provides Context provideContext(App application) { - return application.getApplicationContext(); - } - - @Singleton @Provides Gson provideGson() { - return new Gson(); - } - - @Singleton @Provides OkHttpClient okHttpClient() { - return new OkHttpClient.Builder().addInterceptor(new LogInterceptor()) - .connectTimeout(15, TimeUnit.MINUTES) - .readTimeout(30, TimeUnit.MINUTES) - .writeTimeout(30, TimeUnit.MINUTES) - .build(); - } - - @Singleton @Provides EthereumNetworkRepositoryType provideEthereumNetworkRepository( - PreferenceRepositoryType preferenceRepository, TickerService tickerService) { - return new EthereumNetworkRepository(preferenceRepository, tickerService); - } - - @Singleton @Provides PreferenceRepositoryType providePreferenceRepository(Context context) { - return new SharedPreferenceRepository(context); - } - - @Singleton @Provides TickerService provideTickerService(OkHttpClient httpClient, Gson gson) { - return new TrustWalletTickerService(httpClient, gson); - } - - @Provides AddTokenInteract provideAddTokenInteract(TokenRepositoryType tokenRepository, - WalletRepositoryType walletRepository) { - return new AddTokenInteract(walletRepository, tokenRepository); - } - - @Singleton @Provides PasswordStore passwordStore(Context context) { - return new TrustPasswordStore(context); - } - - @Singleton @Provides RealmManager provideRealmManager() { - return new RealmManager(); - } - - @Provides ApproveService provideApproveService(SendTransactionInteract sendTransactionInteract, - ErrorMapper errorMapper) { - return new ApproveService(sendTransactionInteract, - new MemoryCache<>(BehaviorSubject.create(), new HashMap<>()), errorMapper, Schedulers.io()); - } - - @Provides BuyService provideBuyService(SendTransactionInteract sendTransactionInteract, - ErrorMapper errorMapper, PendingTransactionService pendingTransactionService) { - return new BuyService(sendTransactionInteract, pendingTransactionService, - new MemoryCache<>(BehaviorSubject.create(), new HashMap<>()), errorMapper, Schedulers.io()); - } - - @Singleton @Provides ErrorMapper provideErrorMapper() { - return new ErrorMapper(); - } - - @Provides GasSettingsRouter provideGasSettingsRouter() { - return new GasSettingsRouter(); - } - - @Provides FetchGasSettingsInteract provideFetchGasSettingsInteract( - GasSettingsRepositoryType gasSettingsRepository) { - return new FetchGasSettingsInteract(gasSettingsRepository); - } - - @Provides FindDefaultWalletInteract provideFindDefaultWalletInteract( - WalletRepositoryType walletRepository) { - return new FindDefaultWalletInteract(walletRepository); - } - - @Provides SendTransactionInteract provideSendTransactionInteract( - TransactionRepositoryType transactionRepository, PasswordStore passwordStore) { - return new SendTransactionInteract(transactionRepository, passwordStore); - } - - @Singleton @Provides InAppPurchaseService provideTransactionService(ApproveService approveService, - BuyService buyService, NonceGetter nonceGetter, BalanceService balanceService) { - return new InAppPurchaseService(new MemoryCache<>(BehaviorSubject.create(), new HashMap<>()), - approveService, buyService, nonceGetter, balanceService); - } - - @Singleton @Provides InAppPurchaseInteractor provideTransactionInteractor( - InAppPurchaseService inAppPurchaseService, FindDefaultWalletInteract defaultWalletInteract, - FetchGasSettingsInteract gasSettingsInteract, TransferParser parser) { - return new InAppPurchaseInteractor(inAppPurchaseService, defaultWalletInteract, - gasSettingsInteract, new BigDecimal(BuildConfig.PAYMENT_GAS_LIMIT), parser); - } - - @Provides GetDefaultWalletBalance provideGetDefaultWalletBalance( - WalletRepositoryType walletRepository, - EthereumNetworkRepositoryType ethereumNetworkRepository, - FetchTokensInteract fetchTokensInteract, FindDefaultWalletInteract defaultWalletInteract) { - return new GetDefaultWalletBalance(walletRepository, ethereumNetworkRepository, - fetchTokensInteract, defaultWalletInteract); - } - - @Provides FetchTokensInteract provideFetchTokensInteract(TokenRepositoryType tokenRepository, - DefaultTokenProvider defaultTokenProvider) { - return new FetchTokensInteract(tokenRepository, defaultTokenProvider); - } - - @Provides BalanceService provideBalanceService(GetDefaultWalletBalance getDefaultWalletBalance) { - return getDefaultWalletBalance; - } - - @Provides TransferParser provideTransferParser( - FindDefaultWalletInteract provideFindDefaultWalletInteract, - TokenRepositoryType tokenRepositoryType) { - return new TransferParser(provideFindDefaultWalletInteract, tokenRepositoryType); - } - - @Provides FindDefaultNetworkInteract provideFindDefaultNetworkInteract( - EthereumNetworkRepositoryType ethereumNetworkRepositoryType) { - return new FindDefaultNetworkInteract(ethereumNetworkRepositoryType, - AndroidSchedulers.mainThread()); - } - - @Provides DefaultTokenProvider provideDefaultTokenProvider( - FindDefaultNetworkInteract defaultNetworkInteract, - FindDefaultWalletInteract findDefaultWalletInteract) { - return new BuildConfigDefaultTokenProvider(defaultNetworkInteract, findDefaultWalletInteract); - } - - @Singleton @Provides Calculator provideMessageDigest() { - return new Calculator(); - } - - @Singleton @Provides GasSettingsRepositoryType provideGasSettingsRepository( - EthereumNetworkRepositoryType ethereumNetworkRepository) { - return new GasSettingsRepository(ethereumNetworkRepository); - } - - @Singleton @Provides DataMapper provideDataMapper() { - return new DataMapper(); - } - - @Singleton @Provides @Named("REGISTER_PROOF_GAS_LIMIT") BigDecimal provideRegisterPoaGasLimit() { - return new BigDecimal(BuildConfig.REGISTER_PROOF_GAS_LIMIT); - } - - @Singleton @Provides TransactionFactory provideTransactionFactory(Web3jProvider web3jProvider, - WalletRepositoryType walletRepository, AccountKeystoreService accountKeystoreService, - PasswordStore passwordStore, DefaultTokenProvider defaultTokenProvider, - EthereumNetworkRepositoryType ethereumNetworkRepository, DataMapper dataMapper) { - - return new TransactionFactory(web3jProvider, walletRepository, accountKeystoreService, - passwordStore, defaultTokenProvider, ethereumNetworkRepository, dataMapper); - } - - @Singleton @Provides ProofWriter provideBlockChainWriter(Web3jProvider web3jProvider, - TransactionFactory transactionFactory, - @Named("REGISTER_PROOF_GAS_LIMIT") BigDecimal registerPoaGasLimit, - GasSettingsRepositoryType gasSettingsRepository, - FindDefaultWalletInteract defaultWalletInteract, WalletRepositoryType walletRepositoryType, - EthereumNetworkRepositoryType ethereumNetwork) { - return new BlockChainWriter(web3jProvider, transactionFactory, walletRepositoryType, - defaultWalletInteract, gasSettingsRepository, registerPoaGasLimit, ethereumNetwork); - } - - @Singleton @Provides HashCalculator provideHashCalculator(Calculator calculator) { - return new HashCalculator(BuildConfig.LEADING_ZEROS_ON_PROOF_OF_ATTENTION, calculator); - } - - @Provides TaggedCompositeDisposable provideTaggedCompositeDisposable() { - return new TaggedCompositeDisposable(new HashMap<>()); - } - - @Singleton @Provides ProofOfAttentionService provideProofOfAttentionService( - HashCalculator hashCalculator, ProofWriter proofWriter, - TaggedCompositeDisposable disposables) { - return new ProofOfAttentionService(new MemoryCache<>(BehaviorSubject.create(), new HashMap<>()), - BuildConfig.APPLICATION_ID, hashCalculator, new CompositeDisposable(), proofWriter, - Schedulers.computation(), 12, new BlockchainErrorMapper(), disposables); - } - - @Provides NonceGetter provideNonceGetter(EthereumNetworkRepositoryType networkRepository, - FindDefaultWalletInteract defaultWalletInteract) { - return new NonceGetter(networkRepository, defaultWalletInteract); - } - - @Provides @Singleton AppcoinsOperationsDataSaver provideInAppPurchaseDataSaver(Context context, - List list) { - return new AppcoinsOperationsDataSaver(list, new AppCoinsOperationRepository( - Room.databaseBuilder(context.getApplicationContext(), AppCoinsOperationDatabase.class, - "appcoins_operations_data") - .build() - .appCoinsOperationDao(), new AppCoinsOperationMapper()), - new AppInfoProvider(context, new ImageSaver(context.getFilesDir() + "/app_icons/")), - Schedulers.io(), new CompositeDisposable()); - } - - @Provides OperationSources provideOperationSources( - InAppPurchaseInteractor inAppPurchaseInteractor, - ProofOfAttentionService proofOfAttentionService) { - return new OperationSources(inAppPurchaseInteractor, proofOfAttentionService); - } - - @Provides - List provideAppcoinsOperationListDataSource( - OperationSources operationSources) { - return operationSources.getSources(); - } - - @Provides AirdropChainIdMapper provideAirdropChainIdMapper( - FindDefaultNetworkInteract defaultNetworkInteract) { - return new AirdropChainIdMapper(defaultNetworkInteract); - } - - @Provides AirdropService provideAirdropService(OkHttpClient client, Gson gson) { - AirdropService.Api api = new Retrofit.Builder().baseUrl(BASE_URL) - .client(client) - .addConverterFactory(GsonConverterFactory.create(gson)) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .build() - .create(AirdropService.Api.class); - return new AirdropService(api, gson, Schedulers.io()); - } - - @Singleton @Provides AirdropInteractor provideAirdropInteractor( - PendingTransactionService pendingTransactionService, EthereumNetworkRepositoryType repository, - AirdropService airdropService, FindDefaultWalletInteract findDefaultWalletInteract, - AirdropChainIdMapper airdropChainIdMapper) { - return new AirdropInteractor( - new Airdrop(new AppcoinsTransactionService(pendingTransactionService), - BehaviorSubject.create(), airdropService), findDefaultWalletInteract, - airdropChainIdMapper, repository); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/TransactionDetailModule.java b/app/src/main/java/com/asfoundation/wallet/di/TransactionDetailModule.java deleted file mode 100644 index 6ef0cb03205..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/TransactionDetailModule.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.interact.FindDefaultNetworkInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.router.ExternalBrowserRouter; -import com.asfoundation.wallet.viewmodel.TransactionDetailViewModelFactory; -import dagger.Module; -import dagger.Provides; - -@Module public class TransactionDetailModule { - - @Provides TransactionDetailViewModelFactory provideTransactionDetailViewModelFactory( - FindDefaultNetworkInteract findDefaultNetworkInteract, - FindDefaultWalletInteract findDefaultWalletInteract, - ExternalBrowserRouter externalBrowserRouter) { - return new TransactionDetailViewModelFactory(findDefaultNetworkInteract, - findDefaultWalletInteract, externalBrowserRouter); - } - - @Provides ExternalBrowserRouter externalBrowserRouter() { - return new ExternalBrowserRouter(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/TransactionDetailModule.kt b/app/src/main/java/com/asfoundation/wallet/di/TransactionDetailModule.kt new file mode 100644 index 00000000000..54cf47d1d34 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/TransactionDetailModule.kt @@ -0,0 +1,25 @@ +package com.asfoundation.wallet.di + +import com.asfoundation.wallet.interact.FindDefaultNetworkInteract +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.router.ExternalBrowserRouter +import com.asfoundation.wallet.viewmodel.TransactionDetailViewModelFactory +import dagger.Module +import dagger.Provides +import io.reactivex.disposables.CompositeDisposable + +@Module +class TransactionDetailModule { + @Provides + fun provideTransactionDetailViewModelFactory( + findDefaultNetworkInteract: FindDefaultNetworkInteract, + findDefaultWalletInteract: FindDefaultWalletInteract, + externalBrowserRouter: ExternalBrowserRouter): TransactionDetailViewModelFactory { + return TransactionDetailViewModelFactory(findDefaultNetworkInteract, + findDefaultWalletInteract, externalBrowserRouter, + CompositeDisposable()) + } + + @Provides + fun externalBrowserRouter() = ExternalBrowserRouter() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/di/TransactionsModule.java b/app/src/main/java/com/asfoundation/wallet/di/TransactionsModule.java deleted file mode 100644 index 730c9c7eea7..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/di/TransactionsModule.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.asfoundation.wallet.di; - -import com.asfoundation.wallet.interact.DefaultTokenProvider; -import com.asfoundation.wallet.interact.FetchTransactionsInteract; -import com.asfoundation.wallet.interact.FindDefaultNetworkInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.interact.GetDefaultWalletBalance; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; -import com.asfoundation.wallet.repository.TokenLocalSource; -import com.asfoundation.wallet.repository.TokenRepository; -import com.asfoundation.wallet.repository.TransactionLocalSource; -import com.asfoundation.wallet.repository.TransactionRepositoryType; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import com.asfoundation.wallet.router.AirdropRouter; -import com.asfoundation.wallet.router.ExternalBrowserRouter; -import com.asfoundation.wallet.router.ManageWalletsRouter; -import com.asfoundation.wallet.router.MyAddressRouter; -import com.asfoundation.wallet.router.MyTokensRouter; -import com.asfoundation.wallet.router.SendRouter; -import com.asfoundation.wallet.router.SettingsRouter; -import com.asfoundation.wallet.router.TransactionDetailRouter; -import com.asfoundation.wallet.service.TickerService; -import com.asfoundation.wallet.service.TokenExplorerClientType; -import com.asfoundation.wallet.transactions.TransactionsMapper; -import com.asfoundation.wallet.ui.iab.AppcoinsOperationsDataSaver; -import com.asfoundation.wallet.viewmodel.TransactionsViewModelFactory; -import dagger.Module; -import dagger.Provides; -import io.reactivex.schedulers.Schedulers; -import okhttp3.OkHttpClient; - -@Module class TransactionsModule { - @Provides TransactionsViewModelFactory provideTransactionsViewModelFactory( - FindDefaultNetworkInteract findDefaultNetworkInteract, - FindDefaultWalletInteract findDefaultWalletInteract, - FetchTransactionsInteract fetchTransactionsInteract, ManageWalletsRouter manageWalletsRouter, - SettingsRouter settingsRouter, SendRouter sendRouter, - TransactionDetailRouter transactionDetailRouter, MyAddressRouter myAddressRouter, - MyTokensRouter myTokensRouter, ExternalBrowserRouter externalBrowserRouter, - DefaultTokenProvider defaultTokenProvider, GetDefaultWalletBalance getDefaultWalletBalance, - TransactionsMapper transactionsMapper, AirdropRouter airdropRouter, - AppcoinsOperationsDataSaver operationsDataSaver) { - return new TransactionsViewModelFactory(findDefaultNetworkInteract, findDefaultWalletInteract, - fetchTransactionsInteract, manageWalletsRouter, settingsRouter, sendRouter, - transactionDetailRouter, myAddressRouter, myTokensRouter, externalBrowserRouter, - defaultTokenProvider, getDefaultWalletBalance, transactionsMapper, airdropRouter, operationsDataSaver); - } - - @Provides FetchTransactionsInteract provideFetchTransactionsInteract( - TransactionRepositoryType transactionRepository) { - return new FetchTransactionsInteract(transactionRepository); - } - - @Provides ManageWalletsRouter provideManageWalletsRouter() { - return new ManageWalletsRouter(); - } - - @Provides SettingsRouter provideSettingsRouter() { - return new SettingsRouter(); - } - - @Provides SendRouter provideSendRouter() { - return new SendRouter(); - } - - @Provides TransactionDetailRouter provideTransactionDetailRouter() { - return new TransactionDetailRouter(); - } - - @Provides MyAddressRouter provideMyAddressRouter() { - return new MyAddressRouter(); - } - - @Provides MyTokensRouter provideMyTokensRouter() { - return new MyTokensRouter(); - } - - @Provides ExternalBrowserRouter provideExternalBrowserRouter() { - return new ExternalBrowserRouter(); - } - - @Provides TokenRepository provideTokenRepository(OkHttpClient okHttpClient, - EthereumNetworkRepositoryType ethereumNetworkRepository, - WalletRepositoryType walletRepository, TokenExplorerClientType tokenExplorerClientType, - TokenLocalSource tokenLocalSource, TransactionLocalSource inDiskCache, - TickerService tickerService) { - return new TokenRepository(okHttpClient, ethereumNetworkRepository, walletRepository, - tokenExplorerClientType, tokenLocalSource, inDiskCache, tickerService); - } - - @Provides TransactionsMapper provideTransactionsMapper( - DefaultTokenProvider defaultTokenProvider, AppcoinsOperationsDataSaver operationsDataSaver) { - return new TransactionsMapper(defaultTokenProvider, operationsDataSaver, Schedulers.io()); - } - - @Provides AirdropRouter provideAirdropRouter() { - return new AirdropRouter(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/di/TransactionsModule.kt b/app/src/main/java/com/asfoundation/wallet/di/TransactionsModule.kt new file mode 100644 index 00000000000..dc6bfc48675 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/di/TransactionsModule.kt @@ -0,0 +1,63 @@ +package com.asfoundation.wallet.di + +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.interact.TransactionViewInteract +import com.asfoundation.wallet.navigator.TransactionViewNavigator +import com.asfoundation.wallet.router.* +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.transactions.TransactionsAnalytics +import com.asfoundation.wallet.ui.AppcoinsApps +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.viewmodel.TransactionsViewModelFactory +import dagger.Module +import dagger.Provides + +@Module +internal class TransactionsModule { + @Provides + fun provideTransactionsViewModelFactory(applications: AppcoinsApps, + analytics: TransactionsAnalytics, + transactionViewNavigator: TransactionViewNavigator, + transactionViewInteract: TransactionViewInteract, + walletsEventSender: WalletsEventSender, + supportInteractor: SupportInteractor, + formatter: CurrencyFormatUtils): TransactionsViewModelFactory { + return TransactionsViewModelFactory(applications, analytics, transactionViewNavigator, + transactionViewInteract, walletsEventSender, supportInteractor, formatter) + } + + @Provides + fun provideTransactionsViewNavigator(settingsRouter: SettingsRouter, sendRouter: SendRouter, + transactionDetailRouter: TransactionDetailRouter, + myAddressRouter: MyAddressRouter, + balanceRouter: BalanceRouter, + externalBrowserRouter: ExternalBrowserRouter, + topUpRouter: TopUpRouter): TransactionViewNavigator { + return TransactionViewNavigator(settingsRouter, sendRouter, transactionDetailRouter, + myAddressRouter, balanceRouter, externalBrowserRouter, topUpRouter) + } + + @Provides + fun provideSettingsRouter() = SettingsRouter() + + @Provides + fun provideSendRouter() = SendRouter() + + @Provides + fun provideSendRouterTopUpRouter() = TopUpRouter() + + @Provides + fun provideTransactionDetailRouter() = TransactionDetailRouter() + + @Provides + fun provideMyAddressRouter() = MyAddressRouter() + + @Provides + fun provideMyTokensRouter() = BalanceRouter() + + @Provides + fun provideExternalBrowserRouter() = ExternalBrowserRouter() + + @Provides + fun provideAirdropRouter() = AirdropRouter() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/entity/Address.java b/app/src/main/java/com/asfoundation/wallet/entity/Address.java index 45aa9011307..2447f6cf2e6 100644 --- a/app/src/main/java/com/asfoundation/wallet/entity/Address.java +++ b/app/src/main/java/com/asfoundation/wallet/entity/Address.java @@ -6,8 +6,6 @@ public class Address { private static final Pattern ignoreCaseAddrPattern = Pattern.compile("(?i)^(0x)?[0-9a-f]{40}$"); - private static final Pattern lowerCaseAddrPattern = Pattern.compile("^(0x)?[0-9a-f]{40}$"); - private static final Pattern upperCaseAddrPattern = Pattern.compile("^(0x)?[0-9A-F]{40}$"); public final String value; @@ -17,8 +15,6 @@ public Address(String value) { public static boolean isAddress(String address) { return !(TextUtils.isEmpty(address) || !ignoreCaseAddrPattern.matcher(address) - .find()) && (lowerCaseAddrPattern.matcher(address) - .find() || upperCaseAddrPattern.matcher(address) .find()); } } diff --git a/app/src/main/java/com/asfoundation/wallet/entity/ApiErrorException.java b/app/src/main/java/com/asfoundation/wallet/entity/ApiErrorException.java deleted file mode 100644 index 8a01ed71cbd..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/entity/ApiErrorException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.asfoundation.wallet.entity; - -public class ApiErrorException extends Exception { - private final ErrorEnvelope errorEnvelope; - - public ApiErrorException(ErrorEnvelope errorEnvelope) { - super(errorEnvelope.message); - - this.errorEnvelope = errorEnvelope; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/entity/AppcToFiatResponseBody.java b/app/src/main/java/com/asfoundation/wallet/entity/AppcToFiatResponseBody.java new file mode 100644 index 00000000000..6bf101bb026 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/entity/AppcToFiatResponseBody.java @@ -0,0 +1,30 @@ +package com.asfoundation.wallet.entity; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.math.BigDecimal; + +@JsonInclude(JsonInclude.Include.NON_NULL) @JsonPropertyOrder({ + "Datetime", "APPC" +}) public class AppcToFiatResponseBody { + + @JsonProperty("Datetime") private String datetime; + @JsonProperty("APPC") private BigDecimal fiatValue; + + @JsonProperty("Datetime") public String getDatetime() { + return datetime; + } + + @JsonProperty("Datetime") public void setDatetime(String datetime) { + this.datetime = datetime; + } + + @JsonProperty("APPC") public BigDecimal getFiatValue() { + return fiatValue; + } + + @JsonProperty("APPC") public void setFiatValue(BigDecimal appc) { + this.fiatValue = appc; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/entity/AutoUpdateResponse.kt b/app/src/main/java/com/asfoundation/wallet/entity/AutoUpdateResponse.kt new file mode 100644 index 00000000000..d4341bfe042 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/entity/AutoUpdateResponse.kt @@ -0,0 +1,11 @@ +package com.asfoundation.wallet.entity + +import com.google.gson.annotations.SerializedName + +data class AutoUpdateResponse(@SerializedName("latest_version") + val latestVersion: LatestVersionResponse, + @SerializedName("black_list") + val blackList: List) + +data class LatestVersionResponse(@SerializedName("version_code") val versionCode: Int, + @SerializedName("min_sdk") val minSdk: Int) diff --git a/app/src/main/java/com/asfoundation/wallet/entity/Balance.java b/app/src/main/java/com/asfoundation/wallet/entity/Balance.java deleted file mode 100644 index 3a37dc185f9..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/entity/Balance.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.asfoundation.wallet.entity; - -public class Balance { - private final String tokenSymbol; - private final long tokenBalance; - private final long fiatBalance; - private final long fiatSymbol; - - public Balance(String tokenSymbol, long tokenBalance, long fiatBalance, long fiatSymbol) { - this.tokenSymbol = tokenSymbol; - this.tokenBalance = tokenBalance; - this.fiatBalance = fiatBalance; - - this.fiatSymbol = fiatSymbol; - } - - public long getFiatBalance() { - return fiatBalance; - } - - public long getFiatSymbol() { - return fiatSymbol; - } - - public String getTokenSymbol() { - return tokenSymbol; - } - - public long getTokenBalance() { - return tokenBalance; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/entity/Balance.kt b/app/src/main/java/com/asfoundation/wallet/entity/Balance.kt new file mode 100644 index 00000000000..292a2c0e914 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/entity/Balance.kt @@ -0,0 +1,16 @@ +package com.asfoundation.wallet.entity + +import java.math.BigDecimal + +data class Balance(val symbol: String, val value: BigDecimal) { + + fun getStringValue(): String { + return value + .stripTrailingZeros() + .toPlainString() + } + + override fun toString(): String { + return "$value $symbol" + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/entity/ConversionResponseBody.java b/app/src/main/java/com/asfoundation/wallet/entity/ConversionResponseBody.java new file mode 100644 index 00000000000..52b65e83839 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/entity/ConversionResponseBody.java @@ -0,0 +1,28 @@ +package com.asfoundation.wallet.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.math.BigDecimal; + +public class ConversionResponseBody { + + private String currency; + @JsonProperty("value") private BigDecimal appcValue; + private String label; + @JsonProperty("sign") private String symbol; + + public String getCurrency() { + return currency; + } + + public BigDecimal getAppcValue() { + return appcValue; + } + + public String getLabel() { + return label; + } + + public String getSymbol() { + return symbol; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/entity/ErrorEnvelope.java b/app/src/main/java/com/asfoundation/wallet/entity/ErrorEnvelope.java index 6b9c967cf8c..3353941ca14 100644 --- a/app/src/main/java/com/asfoundation/wallet/entity/ErrorEnvelope.java +++ b/app/src/main/java/com/asfoundation/wallet/entity/ErrorEnvelope.java @@ -1,7 +1,7 @@ package com.asfoundation.wallet.entity; -import android.support.annotation.Nullable; -import android.support.annotation.StringRes; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import com.asfoundation.wallet.C; public class ErrorEnvelope { diff --git a/app/src/main/java/com/asfoundation/wallet/entity/FiatValueRequest.java b/app/src/main/java/com/asfoundation/wallet/entity/FiatValueRequest.java new file mode 100644 index 00000000000..385331e6621 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/entity/FiatValueRequest.java @@ -0,0 +1,21 @@ +package com.asfoundation.wallet.entity; + +/** + * Created by franciscocalado on 24/07/2018. + */ + +public class FiatValueRequest { + private double appc; + + public FiatValueRequest(double appcValue) { + this.appc = appcValue; + } + + public double getAppc() { + return appc; + } + + public void setAppc(double appc) { + this.appc = appc; + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/entity/GlobalBalance.kt b/app/src/main/java/com/asfoundation/wallet/entity/GlobalBalance.kt new file mode 100644 index 00000000000..eb3a865efe6 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/entity/GlobalBalance.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.entity + +data class GlobalBalance(val appcoinsBalance: Balance, val creditsBalance: Balance, + val etherBalance: Balance, val fiatSymbol: String, + val fiatValue: String, val showAppcoins: Boolean, + val showCredits: Boolean, val showEthereum: Boolean) + diff --git a/app/src/main/java/com/asfoundation/wallet/entity/PendingTransaction.java b/app/src/main/java/com/asfoundation/wallet/entity/PendingTransaction.java index b8d136a7509..87b85663d5e 100644 --- a/app/src/main/java/com/asfoundation/wallet/entity/PendingTransaction.java +++ b/app/src/main/java/com/asfoundation/wallet/entity/PendingTransaction.java @@ -1,5 +1,7 @@ package com.asfoundation.wallet.entity; +import java.util.Objects; + /** * Created by trinkes on 28/02/2018. */ @@ -26,8 +28,8 @@ public PendingTransaction(String hash, Boolean pending) { PendingTransaction that = (PendingTransaction) o; - if (hash != null ? !hash.equals(that.hash) : that.hash != null) return false; - return pending != null ? pending.equals(that.pending) : that.pending == null; + if (!Objects.equals(hash, that.hash)) return false; + return Objects.equals(pending, that.pending); } @Override public String toString() { diff --git a/app/src/main/java/com/asfoundation/wallet/entity/RawTransaction.java b/app/src/main/java/com/asfoundation/wallet/entity/RawTransaction.java index ad1e3348f80..36f374a979d 100644 --- a/app/src/main/java/com/asfoundation/wallet/entity/RawTransaction.java +++ b/app/src/main/java/com/asfoundation/wallet/entity/RawTransaction.java @@ -18,6 +18,7 @@ public class RawTransaction implements Parcelable { @SerializedName("id") public final String hash; public final String blockNumber; public final long timeStamp; + public final long processedTime; public final int nonce; public final String from; public final String to; @@ -29,13 +30,14 @@ public class RawTransaction implements Parcelable { public final TransactionOperation[] operations; public final String error; - public RawTransaction(String hash, String error, String blockNumber, long timeStamp, int nonce, - String from, String to, String value, String gas, String gasPrice, String input, - String gasUsed, TransactionOperation[] operations) { + public RawTransaction(String hash, String error, String blockNumber, long timeStamp, + long processedTime, int nonce, String from, String to, String value, String gas, + String gasPrice, String input, String gasUsed, TransactionOperation[] operations) { this.hash = hash; this.error = error; this.blockNumber = blockNumber; this.timeStamp = timeStamp; + this.processedTime = processedTime; this.nonce = nonce; this.from = from; this.to = to; @@ -52,6 +54,7 @@ protected RawTransaction(Parcel in) { error = in.readString(); blockNumber = in.readString(); timeStamp = in.readLong(); + processedTime = in.readLong(); nonce = in.readInt(); from = in.readString(); to = in.readString(); @@ -79,6 +82,7 @@ protected RawTransaction(Parcel in) { dest.writeString(error); dest.writeString(blockNumber); dest.writeLong(timeStamp); + dest.writeLong(processedTime); dest.writeInt(nonce); dest.writeString(from); dest.writeString(to); diff --git a/app/src/main/java/com/asfoundation/wallet/entity/ServiceErrorException.java b/app/src/main/java/com/asfoundation/wallet/entity/ServiceErrorException.java index 54a5bdb3907..ecdb6e8f79d 100644 --- a/app/src/main/java/com/asfoundation/wallet/entity/ServiceErrorException.java +++ b/app/src/main/java/com/asfoundation/wallet/entity/ServiceErrorException.java @@ -1,6 +1,6 @@ package com.asfoundation.wallet.entity; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; public class ServiceErrorException extends Exception { diff --git a/app/src/main/java/com/asfoundation/wallet/entity/ServiceException.java b/app/src/main/java/com/asfoundation/wallet/entity/ServiceException.java deleted file mode 100644 index 6cf7757dd8c..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/entity/ServiceException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.asfoundation.wallet.entity; - -public class ServiceException extends Exception { - public final ErrorEnvelope error; - - public ServiceException(String message) { - super(message); - - error = new ErrorEnvelope(message); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/entity/SubmitPoAException.kt b/app/src/main/java/com/asfoundation/wallet/entity/SubmitPoAException.kt new file mode 100644 index 00000000000..88029949919 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/entity/SubmitPoAException.kt @@ -0,0 +1,31 @@ +package com.asfoundation.wallet.entity + + +class SubmitPoAException(errorCode: Int = -1) : Exception() { + val error = mapErrorCode(errorCode) + + private fun mapErrorCode(code: Int): Int { + var errorCode = GENERIC_ERROR + when (code) { + 0 -> errorCode = CAMPAIGN_NOT_EXISTENT + 1 -> errorCode = CAMPAIGN_NOT_AVAILABLE + 2 -> errorCode = NOT_ENOUGH_BUDGET + 3 -> errorCode = NOT_AVAILABLE_FOR_COUNTRY + 4 -> errorCode = ALREADY_SUBMITTED_FOR_IP + 5 -> errorCode = ALREADY_SUBMITTED_FOR_WALLET + 6 -> errorCode = INCORRECT_DATA + } + return errorCode + } + + companion object { + const val GENERIC_ERROR = -1 + const val CAMPAIGN_NOT_EXISTENT = 0 + const val CAMPAIGN_NOT_AVAILABLE = 1 + const val NOT_ENOUGH_BUDGET = 2 + const val NOT_AVAILABLE_FOR_COUNTRY = 3 + const val ALREADY_SUBMITTED_FOR_IP = 4 + const val ALREADY_SUBMITTED_FOR_WALLET = 5 + const val INCORRECT_DATA = 6 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/entity/SubmitPoAResponse.kt b/app/src/main/java/com/asfoundation/wallet/entity/SubmitPoAResponse.kt new file mode 100644 index 00000000000..22be432e8e3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/entity/SubmitPoAResponse.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.entity + +import com.google.gson.annotations.SerializedName + +data class SubmitPoAResponse(@SerializedName("txid") val transactionId: String, + @SerializedName("valid") val isValid: Boolean, + @SerializedName("error_code") val errorCode: Int) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/entity/Ticker.java b/app/src/main/java/com/asfoundation/wallet/entity/Ticker.java deleted file mode 100644 index 4f111b27cef..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/entity/Ticker.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.asfoundation.wallet.entity; - -import com.google.gson.annotations.SerializedName; - -public class Ticker { - public String id; - public String name; - public String symbol; - public String price; - @SerializedName("percent_change_24h") public String percentChange24h; -} diff --git a/app/src/main/java/com/asfoundation/wallet/entity/Token.java b/app/src/main/java/com/asfoundation/wallet/entity/Token.java index 83cc185616a..de4e12bf8af 100644 --- a/app/src/main/java/com/asfoundation/wallet/entity/Token.java +++ b/app/src/main/java/com/asfoundation/wallet/entity/Token.java @@ -16,19 +16,15 @@ public class Token implements Parcelable { }; public final TokenInfo tokenInfo; public final BigDecimal balance; - public final long updateBlancaTime; - public TokenTicker ticker; - public Token(TokenInfo tokenInfo, BigDecimal balance, long updateBlancaTime) { + public Token(TokenInfo tokenInfo, BigDecimal balance) { this.tokenInfo = tokenInfo; this.balance = balance; - this.updateBlancaTime = updateBlancaTime; } private Token(Parcel in) { tokenInfo = in.readParcelable(TokenInfo.class.getClassLoader()); balance = new BigDecimal(in.readString()); - updateBlancaTime = in.readLong(); } @Override public int describeContents() { @@ -38,6 +34,5 @@ private Token(Parcel in) { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(tokenInfo, flags); dest.writeString(balance.toString()); - dest.writeLong(updateBlancaTime); } } diff --git a/app/src/main/java/com/asfoundation/wallet/entity/TokenInfo.java b/app/src/main/java/com/asfoundation/wallet/entity/TokenInfo.java index 20b84524654..0735a1a3c3b 100644 --- a/app/src/main/java/com/asfoundation/wallet/entity/TokenInfo.java +++ b/app/src/main/java/com/asfoundation/wallet/entity/TokenInfo.java @@ -17,17 +17,12 @@ public class TokenInfo implements Parcelable { public final String name; public final String symbol; public final int decimals; - public final boolean isEnabled; - public final boolean isAddedManually; - public TokenInfo(String address, String name, String symbol, int decimals, boolean isEnabled, - boolean isAddedManually) { + public TokenInfo(String address, String name, String symbol, int decimals) { this.address = address; this.name = name; this.symbol = symbol; this.decimals = decimals; - this.isEnabled = isEnabled; - this.isAddedManually = isAddedManually; } private TokenInfo(Parcel in) { @@ -35,8 +30,6 @@ private TokenInfo(Parcel in) { name = in.readString(); symbol = in.readString(); decimals = in.readInt(); - isEnabled = in.readInt() == 1; - isAddedManually = in.readInt() == 1; } @Override public int describeContents() { @@ -48,7 +41,5 @@ private TokenInfo(Parcel in) { dest.writeString(name); dest.writeString(symbol); dest.writeInt(decimals); - dest.writeInt(isEnabled ? 1 : 0); - dest.writeInt(isAddedManually ? 1 : 0); } } diff --git a/app/src/main/java/com/asfoundation/wallet/entity/TokenTicker.java b/app/src/main/java/com/asfoundation/wallet/entity/TokenTicker.java deleted file mode 100644 index bb6307aa776..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/entity/TokenTicker.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.asfoundation.wallet.entity; - -import android.os.Parcel; -import android.os.Parcelable; -import com.google.gson.annotations.SerializedName; - -public class TokenTicker implements Parcelable { - public static final Creator CREATOR = new Creator() { - @Override public TokenTicker createFromParcel(Parcel in) { - return new TokenTicker(in); - } - - @Override public TokenTicker[] newArray(int size) { - return new TokenTicker[size]; - } - }; - public final String id; - public final String contract; - public final String price; - @SerializedName("percent_change_24h") public final String percentChange24h; - public final String image; - - public TokenTicker(String id, String contract, String price, String percentChange24h, - String image) { - this.id = id; - this.contract = contract; - this.price = price; - this.percentChange24h = percentChange24h; - this.image = image; - } - - private TokenTicker(Parcel in) { - id = in.readString(); - contract = in.readString(); - price = in.readString(); - percentChange24h = in.readString(); - image = in.readString(); - } - - @Override public int describeContents() { - return 0; - } - - @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(id); - dest.writeString(contract); - dest.writeString(price); - dest.writeString(percentChange24h); - dest.writeString(image); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/entity/TransactionBuilder.java b/app/src/main/java/com/asfoundation/wallet/entity/TransactionBuilder.java index 4985048e718..688d1ea7358 100644 --- a/app/src/main/java/com/asfoundation/wallet/entity/TransactionBuilder.java +++ b/app/src/main/java/com/asfoundation/wallet/entity/TransactionBuilder.java @@ -2,7 +2,6 @@ import android.os.Parcel; import android.os.Parcelable; -import com.asf.wallet.BuildConfig; import com.asfoundation.wallet.repository.TokenRepository; import com.asfoundation.wallet.util.BalanceUtils; import io.reactivex.annotations.NonNull; @@ -22,6 +21,7 @@ public class TransactionBuilder implements Parcelable { return new TransactionBuilder[size]; } }; + private final long chainId; private String contractAddress; private int decimals; private String symbol; @@ -30,10 +30,46 @@ public class TransactionBuilder implements Parcelable { private String fromAddress; private BigDecimal amount = BigDecimal.ZERO; private byte[] data; + private byte[] appcoinsData; private GasSettings gasSettings; - private long chainId; private String skuId; + private String type; + private String origin; + private String domain; + private String payload; private String iabContract; + private String callbackUrl; + private String orderReference; + private String originalOneStepValue; + private String originalOneStepCurrency; + private String referrerUrl; + private String productName; + + public TransactionBuilder(TransactionBuilder transactionBuilder) { + this.contractAddress = transactionBuilder.contractAddress; + this.decimals = transactionBuilder.decimals; + this.symbol = transactionBuilder.symbol; + this.shouldSendToken = transactionBuilder.shouldSendToken; + this.toAddress = transactionBuilder.toAddress; + this.fromAddress = transactionBuilder.fromAddress; + this.amount = transactionBuilder.amount; + this.data = transactionBuilder.data; + this.appcoinsData = transactionBuilder.appcoinsData; + this.gasSettings = transactionBuilder.gasSettings; + this.chainId = transactionBuilder.chainId; + this.skuId = transactionBuilder.skuId; + this.type = transactionBuilder.type; + this.origin = transactionBuilder.origin; + this.domain = transactionBuilder.domain; + this.payload = transactionBuilder.payload; + this.iabContract = transactionBuilder.iabContract; + this.callbackUrl = transactionBuilder.callbackUrl; + this.orderReference = transactionBuilder.orderReference; + this.originalOneStepValue = transactionBuilder.originalOneStepValue; + this.originalOneStepCurrency = transactionBuilder.originalOneStepCurrency; + this.referrerUrl = transactionBuilder.referrerUrl; + this.productName = transactionBuilder.productName; + } public TransactionBuilder(@NonNull TokenInfo tokenInfo) { contractAddress(tokenInfo.address).decimals(tokenInfo.decimals) @@ -59,10 +95,22 @@ private TransactionBuilder(Parcel in) { gasSettings = in.readParcelable(GasSettings.class.getClassLoader()); chainId = in.readLong(); skuId = in.readString(); + type = in.readString(); + origin = in.readString(); + domain = in.readString(); + payload = in.readString(); + callbackUrl = in.readString(); + orderReference = in.readString(); + originalOneStepValue = in.readString(); + originalOneStepCurrency = in.readString(); + referrerUrl = in.readString(); + productName = in.readString(); } public TransactionBuilder(String symbol, String contractAddress, Long chainId, String toAddress, - BigDecimal amount, String skuId, int decimals) { + BigDecimal amount, String skuId, int decimals, String type, String origin, String domain, + String payload, String callbackUrl, String orderReference, String referrerUrl, + String productName) { this.symbol = symbol; this.contractAddress = contractAddress; this.chainId = chainId == null ? NO_CHAIN_ID : chainId; @@ -71,15 +119,31 @@ public TransactionBuilder(String symbol, String contractAddress, Long chainId, S this.skuId = skuId; this.shouldSendToken = false; this.decimals = decimals; + this.type = type; + this.origin = origin; + this.domain = domain; + this.payload = payload; + this.callbackUrl = callbackUrl; + this.orderReference = orderReference; + this.referrerUrl = referrerUrl; + this.productName = productName; } public TransactionBuilder(String symbol, String contractAddress, Long chainId, String receiverAddress, BigDecimal tokenTransferAmount, String skuId, int decimals, - String iabContract) { - this(symbol, contractAddress, chainId, receiverAddress, tokenTransferAmount, skuId, decimals); + String iabContract, String type, String origin, String domain, String payload, + String callbackUrl, String orderReference, String referrerUrl, String productName) { + this(symbol, contractAddress, chainId, receiverAddress, tokenTransferAmount, skuId, decimals, + type, origin, domain, payload, callbackUrl, orderReference, referrerUrl, productName); this.iabContract = iabContract; } + public TransactionBuilder(String symbol, String contractAddress, Long chainId, + String receiverAddress, BigDecimal tokenTransferAmount, int decimals) { + this(symbol, contractAddress, chainId, receiverAddress, tokenTransferAmount, "", decimals, "", + null, "", "", "", "", null, null); + } + public String getIabContract() { return iabContract; } @@ -167,6 +231,15 @@ public byte[] data() { } } + public TransactionBuilder appcoinsData(byte[] appcoinsData) { + this.appcoinsData = appcoinsData; + return this; + } + + public byte[] appcoinsData() { + return appcoinsData; + } + public TransactionBuilder gasSettings(GasSettings gasSettings) { this.gasSettings = gasSettings; return this; @@ -185,6 +258,34 @@ public String fromAddress() { return fromAddress; } + public String getType() { + return type; + } + + public String getOrigin() { + return origin; + } + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public String getPayload() { + return payload; + } + @Override public String toString() { return "TransactionBuilder{" + "contractAddress='" @@ -207,11 +308,79 @@ public String fromAddress() { + amount + ", data=" + Arrays.toString(data) + + ", appcoinsData=" + + Arrays.toString(appcoinsData) + ", gasSettings=" + gasSettings + + ", chainId=" + + chainId + + ", skuId='" + + skuId + + '\'' + + ", type='" + + type + + '\'' + + ", origin='" + + origin + + '\'' + + ", domain='" + + domain + + '\'' + + ", payload='" + + payload + + '\'' + + ", iabContract='" + + iabContract + + '\'' + + ", callbackUrl='" + + callbackUrl + + '\'' + + ", orderReference='" + + orderReference + + '\'' + + ", originalOneStepValue='" + + originalOneStepValue + + '\'' + + ", originalOneStepCurrency='" + + originalOneStepCurrency + + '\'' + + ", referrerUrl='" + + referrerUrl + + '\'' + + ", productName='" + + productName + + '\'' + '}'; } + public String getReferrerUrl() { + return referrerUrl; + } + + public void setReferrerUrl(String referrerUrl) { + this.referrerUrl = referrerUrl; + } + + public String getCallbackUrl() { + return callbackUrl; + } + + public String getOriginalOneStepValue() { + return originalOneStepValue; + } + + public void setOriginalOneStepValue(String originalOneStepValue) { + this.originalOneStepValue = originalOneStepValue; + } + + public String getOriginalOneStepCurrency() { + return originalOneStepCurrency; + } + + public void setOriginalOneStepCurrency(String originalOneStepCurrency) { + this.originalOneStepCurrency = originalOneStepCurrency; + } + @Override public int describeContents() { return 0; } @@ -228,6 +397,16 @@ public String fromAddress() { dest.writeParcelable(gasSettings, flags); dest.writeLong(chainId); dest.writeString(skuId); + dest.writeString(type); + dest.writeString(origin); + dest.writeString(domain); + dest.writeString(payload); + dest.writeString(callbackUrl); + dest.writeString(orderReference); + dest.writeString(originalOneStepValue); + dest.writeString(originalOneStepCurrency); + dest.writeString(referrerUrl); + dest.writeString(productName); } public byte[] approveData() { @@ -235,9 +414,7 @@ public byte[] approveData() { return TokenRepository.createTokenApproveData(iabContract, amount.multiply(base.pow(decimals))); } - public byte[] buyData(String tokenAddress) { - BigDecimal base = new BigDecimal("10"); - return TokenRepository.buyData(toAddress, BuildConfig.DEFAULT_STORE_ADREESS, - BuildConfig.DEFAULT_OEM_ADREESS, skuId, amount.multiply(base.pow(decimals)), tokenAddress); + public String getOrderReference() { + return orderReference; } } diff --git a/app/src/main/java/com/asfoundation/wallet/entity/WalletHistory.java b/app/src/main/java/com/asfoundation/wallet/entity/WalletHistory.java new file mode 100644 index 00000000000..9a37d8aa9da --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/entity/WalletHistory.java @@ -0,0 +1,254 @@ +package com.asfoundation.wallet.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) public class WalletHistory { + + @JsonProperty("result") private List result; + + public List getResult() { + return result; + } + + public void setResult(List result) { + this.result = result; + } + + public enum Status { + SUCCESS, FAIL + } + + @JsonIgnoreProperties(ignoreUnknown = true) public static class Transaction { + + @JsonProperty("app") private String app; + @JsonProperty("sku") private String sku; + @JsonProperty("TxID") private String txID; + @JsonProperty("amount") private BigInteger amount; + @JsonProperty("block") private BigInteger block; + @JsonProperty("bonus") private BigDecimal bonus; + @JsonProperty("icon") private String icon; + @JsonProperty("receiver") private String receiver; + @JsonProperty("sender") private String sender; + @JsonProperty("ts") private Date ts; + @JsonProperty("processed_time") private Date processedTime; + @JsonProperty("type") private String type; + @JsonProperty("subtype") private String subType; + @JsonProperty("title") private String title; + @JsonProperty("description") private String description; + @JsonProperty("perk") private String perk; + @JsonProperty("status") private Status status; + @JsonProperty("operations") private List operations; + + public List getOperations() { + return operations; + } + + public void setOperations(List operations) { + this.operations = operations; + } + + public String getSku() { + return sku; + } + + public void setSku(String sku) { + this.sku = sku; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getTxID() { + return txID; + } + + public void setTxID(String txID) { + this.txID = txID; + } + + public BigInteger getAmount() { + return amount; + } + + public void setAmount(BigInteger amount) { + this.amount = amount; + } + + public BigInteger getBlock() { + return block; + } + + public void setBlock(BigInteger block) { + this.block = block; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public String getReceiver() { + return receiver; + } + + public void setReceiver(String receiver) { + this.receiver = receiver; + } + + public BigDecimal getBonus() { + return bonus; + } + + public void setBonus(BigDecimal bonus) { + this.bonus = bonus; + } + + public String getSender() { + return sender; + } + + public void setSender(String sender) { + this.sender = sender; + } + + public Date getTs() { + return ts; + } + + public void setTs(Date ts) { + this.ts = ts; + } + + public Date getProcessedTime() { + return processedTime; + } + + public void setProcessedTime(Date processedTime) { + this.processedTime = processedTime; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override public String toString() { + return "Result{" + + "txID='" + + txID + + '\'' + + ", amount=" + + amount + + ", block=" + + block + + ", receiver='" + + receiver + + '\'' + + ", sender='" + + sender + + '\'' + + ", ts='" + + ts + + '\'' + + ", type='" + + type + + '\'' + + '}'; + } + + public String getSubType() { + return subType; + } + + public void setSubType(String subType) { + this.subType = subType; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getPerk() { + return perk; + } + + public void setPerk(String perk) { + this.perk = perk; + } + } + + public static class Operation { + @JsonProperty("TxID") private String transactionId; + private String fee; + private String receiver; + private String sender; + + public String getTransactionId() { + return transactionId; + } + + public void setTransactionId(String transactionId) { + this.transactionId = transactionId; + } + + public String getFee() { + return fee; + } + + public void setFee(String fee) { + this.fee = fee; + } + + public String getSender() { + return sender; + } + + public void setSender(String sender) { + this.sender = sender; + } + + public String getReceiver() { + return receiver; + } + + public void setReceiver(String receiver) { + this.receiver = receiver; + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/entity/WalletRequestCodeResponse.kt b/app/src/main/java/com/asfoundation/wallet/entity/WalletRequestCodeResponse.kt new file mode 100644 index 00000000000..d5147bb7c5e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/entity/WalletRequestCodeResponse.kt @@ -0,0 +1,3 @@ +package com.asfoundation.wallet.entity + +data class WalletRequestCodeResponse(val phone: String) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/entity/WalletStatus.kt b/app/src/main/java/com/asfoundation/wallet/entity/WalletStatus.kt new file mode 100644 index 00000000000..875ee81f275 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/entity/WalletStatus.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.entity + +data class WalletStatus( + val walletAddress: String, + val verified: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/entity/WalletValidationException.kt b/app/src/main/java/com/asfoundation/wallet/entity/WalletValidationException.kt new file mode 100644 index 00000000000..e4b00b1794b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/entity/WalletValidationException.kt @@ -0,0 +1,3 @@ +package com.asfoundation.wallet.entity + +class WalletValidationException(val status: String) : Exception() \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/identification/IdsRepository.kt b/app/src/main/java/com/asfoundation/wallet/identification/IdsRepository.kt new file mode 100644 index 00000000000..acba3f9dab2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/identification/IdsRepository.kt @@ -0,0 +1,34 @@ +package com.asfoundation.wallet.identification + +import android.content.ContentResolver +import android.provider.Settings +import com.asfoundation.wallet.billing.partners.InstallerService +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import io.reactivex.Single + +class IdsRepository(private val contentResolver: ContentResolver, + private val sharedPreferencesRepository: PreferencesRepositoryType, + private val installerService: InstallerService) { + + fun getAndroidId(): String { + var androidId = sharedPreferencesRepository.getAndroidId() + if (androidId.isNotEmpty()) { + return androidId + } + androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) + + sharedPreferencesRepository.setAndroidId(androidId) + return androidId + } + + fun getActiveWalletAddress(): String { + return sharedPreferencesRepository.getCurrentWalletAddress() ?: "" + } + + fun getGamificationLevel() = sharedPreferencesRepository.getGamificationLevel() + + fun getInstallerPackage(packageName: String) : Single { + return installerService.getInstallerPackageName(packageName) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/AddTokenInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/AddTokenInteract.java deleted file mode 100644 index fb4fdd89d40..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/interact/AddTokenInteract.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.asfoundation.wallet.interact; - -import com.asfoundation.wallet.repository.TokenRepositoryType; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import io.reactivex.Completable; -import io.reactivex.android.schedulers.AndroidSchedulers; - -public class AddTokenInteract { - private final TokenRepositoryType tokenRepository; - private final WalletRepositoryType walletRepository; - - public AddTokenInteract(WalletRepositoryType walletRepository, - TokenRepositoryType tokenRepository) { - this.walletRepository = walletRepository; - this.tokenRepository = tokenRepository; - } - - public Completable add(String address, String symbol, int decimals) { - return walletRepository.getDefaultWallet() - .flatMapCompletable( - wallet -> tokenRepository.addToken(wallet, address, symbol, decimals, true) - .observeOn(AndroidSchedulers.mainThread())); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/AutoUpdateInteract.kt b/app/src/main/java/com/asfoundation/wallet/interact/AutoUpdateInteract.kt new file mode 100644 index 00000000000..98b1ccb8d0c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/AutoUpdateInteract.kt @@ -0,0 +1,104 @@ +package com.asfoundation.wallet.interact + +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import com.asf.wallet.R +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.repository.AutoUpdateRepository +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction +import com.asfoundation.wallet.viewmodel.AutoUpdateModel +import io.reactivex.Completable +import io.reactivex.Single + +class AutoUpdateInteract(private val autoUpdateRepository: AutoUpdateRepository, + private val walletVersionCode: Int, private val deviceSdk: Int, + private val packageManager: PackageManager, + private val walletPackageName: String, + private val sharedPreferencesRepository: PreferencesRepositoryType) { + + fun getAutoUpdateModel(invalidateCache: Boolean = true): Single { + return autoUpdateRepository.loadAutoUpdateModel(invalidateCache) + } + + fun hasSoftUpdate(updateVersionCode: Int, updatedMinSdk: Int): Boolean { + return walletVersionCode < updateVersionCode && deviceSdk >= updatedMinSdk + } + + fun isHardUpdateRequired(blackList: List, updateVersionCode: Int, + updateMinSdk: Int): Boolean { + return blackList.contains(walletVersionCode) && hasSoftUpdate(updateVersionCode, updateMinSdk) + } + + fun retrieveRedirectUrl(): String { + return if (isAptoideInstalled()) String.format(APTOIDE_APP_VIEW_URL, walletPackageName) + else String.format(PLAY_APP_VIEW_URL, walletPackageName) + } + + fun buildUpdateIntent(): Intent { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(retrieveRedirectUrl())) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val appsList = + packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) + appsList.let { + for (info in appsList) { + if (info.activityInfo.packageName == APTOIDE_PACKAGE_NAME) { + intent.setPackage(info.activityInfo.packageName) + break + } + } + } + return intent + } + + fun getUnwatchedUpdateNotification(): Single { + return getAutoUpdateModel(false) + .flatMap { updateModel -> + sharedPreferencesRepository.getAutoUpdateCardDismissedVersion() + .map { + hasSoftUpdate(updateModel.updateVersionCode, + updateModel.updateMinSdk) && updateModel.updateVersionCode != it + } + } + .map { shouldShow -> + UpdateNotification( + R.string.update_wallet_soft_title, + R.string.update_wallet_soft_body, + R.string.update_button, CardNotificationAction.UPDATE, + R.raw.soft_hard_update_animation).takeIf { shouldShow } ?: EmptyNotification() + } + } + + private fun isAptoideInstalled(): Boolean { + return try { + packageManager.getApplicationInfo(APTOIDE_PACKAGE_NAME, 0) + .enabled + } catch (exception: PackageManager.NameNotFoundException) { + false + } + } + + fun shouldShowNotification(): Boolean { + val savedTime = sharedPreferencesRepository.getUpdateNotificationSeenTime() + val currentTime = System.currentTimeMillis() + val timeToShowNextNotificationInMillis = 3600000 * 12 + return currentTime >= savedTime + timeToShowNextNotificationInMillis + } + + fun saveSeenUpdateNotification() = + sharedPreferencesRepository.setUpdateNotificationSeenTime(System.currentTimeMillis()) + + fun dismissNotification(): Completable { + return getAutoUpdateModel(false) + .flatMapCompletable { + sharedPreferencesRepository.saveAutoUpdateCardDismiss(it.updateVersionCode) + } + } + + companion object { + private const val APTOIDE_PACKAGE_NAME = "cm.aptoide.pt" + private const val APTOIDE_APP_VIEW_URL = "aptoideinstall://package=%s&show_install_popup=false" + const val PLAY_APP_VIEW_URL = "https://play.google.com/store/apps/details?id=%s" + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/BalanceGetter.kt b/app/src/main/java/com/asfoundation/wallet/interact/BalanceGetter.kt new file mode 100644 index 00000000000..8148bbb8ad1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/BalanceGetter.kt @@ -0,0 +1,11 @@ +package com.asfoundation.wallet.interact + +import io.reactivex.Single +import java.math.BigDecimal + + +interface BalanceGetter { + fun getBalance(address: String): Single + + fun getBalance(): Single +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/BuildConfigDefaultTokenProvider.java b/app/src/main/java/com/asfoundation/wallet/interact/BuildConfigDefaultTokenProvider.java index d933d31165f..be5f27078e3 100644 --- a/app/src/main/java/com/asfoundation/wallet/interact/BuildConfigDefaultTokenProvider.java +++ b/app/src/main/java/com/asfoundation/wallet/interact/BuildConfigDefaultTokenProvider.java @@ -3,81 +3,43 @@ import com.asf.wallet.BuildConfig; import com.asfoundation.wallet.entity.NetworkInfo; import com.asfoundation.wallet.entity.TokenInfo; -import com.asfoundation.wallet.entity.Wallet; import io.reactivex.Single; - -import static com.asfoundation.wallet.C.CLASSIC_NETWORK_NAME; -import static com.asfoundation.wallet.C.ETC_SYMBOL; -import static com.asfoundation.wallet.C.ETHER_DECIMALS; -import static com.asfoundation.wallet.C.ETH_SYMBOL; -import static com.asfoundation.wallet.C.KOVAN_NETWORK_NAME; -import static com.asfoundation.wallet.C.POA_NETWORK_NAME; -import static com.asfoundation.wallet.C.POA_SYMBOL; -import static com.asfoundation.wallet.C.SOKOL_NETWORK_NAME; +import org.jetbrains.annotations.NotNull; /** * Created by trinkes on 07/02/2018. */ public class BuildConfigDefaultTokenProvider implements DefaultTokenProvider { - private final FindDefaultNetworkInteract defaultNetworkInteract; private final FindDefaultWalletInteract findDefaultWalletInteract; + private final NetworkInfo defaultNetwork; - public BuildConfigDefaultTokenProvider(FindDefaultNetworkInteract defaultNetworkInteract, - FindDefaultWalletInteract findDefaultWalletInteract) { - this.defaultNetworkInteract = defaultNetworkInteract; + public BuildConfigDefaultTokenProvider(FindDefaultWalletInteract findDefaultWalletInteract, + NetworkInfo defaultNetwork) { this.findDefaultWalletInteract = findDefaultWalletInteract; + this.defaultNetwork = defaultNetwork; } - @Override public Single getDefaultToken() { - return defaultNetworkInteract.find() - .flatMap(networkInfo -> findDefaultWalletInteract.find() - .map(wallet -> getDefaultToken(networkInfo, wallet))); - } - - @Override public Single getAdsAddress(int chainId) { - return Single.just(getDefaultAdsAddress(chainId)); - } - - private String getDefaultAdsAddress(int chainId) { - switch (chainId) { - case 3: - return BuildConfig.ROPSTEN_NETWORK_ASF_ADS_CONTRACT_ADDRESS; - default: - return BuildConfig.MAIN_NETWORK_ASF_ADS_CONTRACT_ADDRESS; - } + @NotNull @Override public Single getDefaultToken() { + return findDefaultWalletInteract.find() + .map(wallet -> getDefaultToken(defaultNetwork)); } - private TokenInfo getDefaultToken(NetworkInfo networkInfo, Wallet wallet) { + @NotNull private TokenInfo getDefaultToken(@NotNull NetworkInfo networkInfo) { switch (networkInfo.chainId) { // MAIN case 1: default: return new TokenInfo(BuildConfig.MAIN_NETWORK_DEFAULT_TOKEN_ADDRESS, BuildConfig.MAIN_NETWORK_DEFAULT_TOKEN_NAME, - BuildConfig.MAIN_NETWORK_DEFAULT_TOKEN_SYMBOL, - BuildConfig.MAIN_NETWORK_DEFAULT_TOKEN_DECIMALS, true, false); - // CLASSIC - case 61: - return new TokenInfo(wallet.address, CLASSIC_NETWORK_NAME, ETC_SYMBOL, ETHER_DECIMALS, true, - false); - // POA - case 99: - return new TokenInfo(wallet.address, POA_NETWORK_NAME, POA_SYMBOL, ETHER_DECIMALS, true, - false); - // KOVAN - case 42: - return new TokenInfo(wallet.address, KOVAN_NETWORK_NAME, ETH_SYMBOL, ETHER_DECIMALS, true, - false); + BuildConfig.MAIN_NETWORK_DEFAULT_TOKEN_SYMBOL.toLowerCase(), + BuildConfig.MAIN_NETWORK_DEFAULT_TOKEN_DECIMALS); // ROPSTEN case 3: return new TokenInfo(BuildConfig.ROPSTEN_DEFAULT_TOKEN_ADDRESS, - BuildConfig.ROPSTEN_DEFAULT_TOKEN_NAME, BuildConfig.ROPSTEN_DEFAULT_TOKEN_SYMBOL, - BuildConfig.ROPSTEN_DEFAULT_TOKEN_DECIMALS, true, false); - // SOKOL - case 77: - return new TokenInfo(wallet.address, SOKOL_NETWORK_NAME, ETH_SYMBOL, ETHER_DECIMALS, true, - false); + BuildConfig.ROPSTEN_DEFAULT_TOKEN_NAME, + BuildConfig.ROPSTEN_DEFAULT_TOKEN_SYMBOL.toLowerCase(), + BuildConfig.ROPSTEN_DEFAULT_TOKEN_DECIMALS); } } } diff --git a/app/src/main/java/com/asfoundation/wallet/interact/CardNotificationsInteractor.kt b/app/src/main/java/com/asfoundation/wallet/interact/CardNotificationsInteractor.kt new file mode 100644 index 00000000000..a877691b088 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/CardNotificationsInteractor.kt @@ -0,0 +1,46 @@ +package com.asfoundation.wallet.interact + +import com.asfoundation.wallet.backup.BackupInteractContract +import com.asfoundation.wallet.backup.BackupNotification +import com.asfoundation.wallet.promotions.PromotionNotification +import com.asfoundation.wallet.promotions.PromotionsInteractorContract +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.referrals.ReferralInteractorContract +import com.asfoundation.wallet.referrals.ReferralNotification +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.functions.Function4 + +class CardNotificationsInteractor( + private val referralInteractor: ReferralInteractorContract, + private val autoUpdateInteract: AutoUpdateInteract, + private val backupInteract: BackupInteractContract, + private val promotionsInteractorContract: PromotionsInteractorContract) { + + + fun getCardNotifications(): Single> { + return Single.zip(referralInteractor.getUnwatchedPendingBonusNotification(), + autoUpdateInteract.getUnwatchedUpdateNotification(), + backupInteract.getUnwatchedBackupNotification(), + promotionsInteractorContract.getUnwatchedPromotionNotification(), + Function4 { referralNotification: CardNotification, updateNotification: CardNotification, backupNotification: CardNotification, promotionNotification: CardNotification -> + val list = ArrayList() + if (referralNotification !is EmptyNotification) list.add(referralNotification) + if (backupNotification !is EmptyNotification) list.add(backupNotification) + if (updateNotification !is EmptyNotification) list.add(updateNotification) + if (promotionNotification !is EmptyNotification) list.add(promotionNotification) + list + }) + } + + fun dismissNotification(cardNotification: CardNotification): Completable { + return when (cardNotification) { + is ReferralNotification -> referralInteractor.dismissNotification(cardNotification) + is UpdateNotification -> autoUpdateInteract.dismissNotification() + is BackupNotification -> backupInteract.dismissNotification() + is PromotionNotification -> promotionsInteractorContract.dismissNotification( + cardNotification.id) + else -> Completable.complete() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/ChangeTokenEnableInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/ChangeTokenEnableInteract.java deleted file mode 100644 index eaed7765d0b..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/interact/ChangeTokenEnableInteract.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.asfoundation.wallet.interact; - -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.TokenRepositoryType; -import io.reactivex.Completable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; - -public class ChangeTokenEnableInteract { - private final TokenRepositoryType tokenRepository; - - public ChangeTokenEnableInteract(TokenRepositoryType tokenRepository) { - this.tokenRepository = tokenRepository; - } - - public Completable setEnable(Wallet wallet, Token token) { - return tokenRepository.setEnable(wallet, token, !token.tokenInfo.isEnabled) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/CreateWalletInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/CreateWalletInteract.java deleted file mode 100644 index 23cc8bd58c2..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/interact/CreateWalletInteract.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.asfoundation.wallet.interact; - -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.interact.rx.operator.Operators; -import com.asfoundation.wallet.repository.PasswordStore; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import io.reactivex.Single; - -import static com.asfoundation.wallet.interact.rx.operator.Operators.completableErrorProxy; - -public class CreateWalletInteract { - - private final WalletRepositoryType walletRepository; - private final PasswordStore passwordStore; - - public CreateWalletInteract(WalletRepositoryType walletRepository, PasswordStore passwordStore) { - this.walletRepository = walletRepository; - this.passwordStore = passwordStore; - } - - public Single create() { - return passwordStore.generatePassword() - .flatMap(masterPassword -> walletRepository.createWallet(masterPassword) - .compose(Operators.savePassword(passwordStore, walletRepository, masterPassword)) - .flatMap(wallet -> passwordVerification(wallet, masterPassword))); - } - - private Single passwordVerification(Wallet wallet, String masterPassword) { - return passwordStore.getPassword(wallet) - .flatMap(password -> walletRepository.exportWallet(wallet, password, password) - .flatMap(keyStore -> walletRepository.findWallet(wallet.address))) - .onErrorResumeNext( - throwable -> walletRepository.deleteWallet(wallet.address, masterPassword) - .lift(completableErrorProxy(throwable)) - .toSingle(() -> wallet)); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/DefaultTokenProvider.java b/app/src/main/java/com/asfoundation/wallet/interact/DefaultTokenProvider.java index 87802a418ee..715e5647321 100644 --- a/app/src/main/java/com/asfoundation/wallet/interact/DefaultTokenProvider.java +++ b/app/src/main/java/com/asfoundation/wallet/interact/DefaultTokenProvider.java @@ -9,6 +9,4 @@ public interface DefaultTokenProvider { Single getDefaultToken(); - - Single getAdsAddress(int chainId); } diff --git a/app/src/main/java/com/asfoundation/wallet/interact/DeleteTokenInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/DeleteTokenInteract.java deleted file mode 100644 index 680bc5fdf3e..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/interact/DeleteTokenInteract.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.asfoundation.wallet.interact; - -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.TokenRepositoryType; -import io.reactivex.Completable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; - -public class DeleteTokenInteract { - private final TokenRepositoryType tokenRepository; - - public DeleteTokenInteract(TokenRepositoryType tokenRepository) { - this.tokenRepository = tokenRepository; - } - - public Completable delete(Wallet wallet, Token token) { - return tokenRepository.delete(wallet, token) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/DeleteWalletInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/DeleteWalletInteract.java deleted file mode 100644 index 85b62f20ea4..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/interact/DeleteWalletInteract.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.asfoundation.wallet.interact; - -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.PasswordStore; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; - -/** - * Delete and fetchTokens wallets - */ -public class DeleteWalletInteract { - private final WalletRepositoryType walletRepository; - private final PasswordStore passwordStore; - - public DeleteWalletInteract(WalletRepositoryType walletRepository, PasswordStore passwordStore) { - this.walletRepository = walletRepository; - this.passwordStore = passwordStore; - } - - public Single delete(Wallet wallet) { - return passwordStore.getPassword(wallet) - .flatMapCompletable(password -> walletRepository.deleteWallet(wallet.address, password)) - .andThen(walletRepository.fetchWallets()) - .observeOn(AndroidSchedulers.mainThread()); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/DeleteWalletInteract.kt b/app/src/main/java/com/asfoundation/wallet/interact/DeleteWalletInteract.kt new file mode 100644 index 00000000000..0aee4252a44 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/DeleteWalletInteract.kt @@ -0,0 +1,24 @@ +package com.asfoundation.wallet.interact + +import com.asfoundation.wallet.repository.PasswordStore +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.repository.WalletRepositoryType +import io.reactivex.Completable + +/** + * Delete and fetchTokens wallets + */ +class DeleteWalletInteract(private val walletRepository: WalletRepositoryType, + private val passwordStore: PasswordStore, + private val preferencesRepositoryType: PreferencesRepositoryType) { + + fun delete(address: String): Completable { + return passwordStore.getPassword(address) + .flatMapCompletable { walletRepository.deleteWallet(address, it) } + .andThen(preferencesRepositoryType.removeWalletValidationStatus(address)) + .andThen(preferencesRepositoryType.removeWalletRestoreBackup(address)) + .andThen(preferencesRepositoryType.removeBackupNotificationSeenTime(address)) + } + + fun hasAuthenticationPermission() = preferencesRepositoryType.hasAuthenticationPermission() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/EmptyNotification.kt b/app/src/main/java/com/asfoundation/wallet/interact/EmptyNotification.kt new file mode 100644 index 00000000000..c18d0ddb899 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/EmptyNotification.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.interact + +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction + +class EmptyNotification : CardNotification(-1, -1, -1, -1, CardNotificationAction.DISMISS) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/ExportWalletInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/ExportWalletInteract.java deleted file mode 100644 index be294dfe006..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/interact/ExportWalletInteract.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.asfoundation.wallet.interact; - -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.PasswordStore; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; - -public class ExportWalletInteract { - - private final WalletRepositoryType walletRepository; - private final PasswordStore passwordStore; - - public ExportWalletInteract(WalletRepositoryType walletRepository, PasswordStore passwordStore) { - this.walletRepository = walletRepository; - this.passwordStore = passwordStore; - } - - public Single export(Wallet wallet, String backupPassword) { - return passwordStore.getPassword(wallet) - .flatMap(password -> walletRepository.exportWallet(wallet, password, backupPassword)) - .observeOn(AndroidSchedulers.mainThread()); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/ExportWalletInteract.kt b/app/src/main/java/com/asfoundation/wallet/interact/ExportWalletInteract.kt new file mode 100644 index 00000000000..d79b265e733 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/ExportWalletInteract.kt @@ -0,0 +1,17 @@ +package com.asfoundation.wallet.interact + +import com.asfoundation.wallet.repository.PasswordStore +import com.asfoundation.wallet.repository.WalletRepositoryType +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers + +class ExportWalletInteract(private val walletRepository: WalletRepositoryType, + private val passwordStore: PasswordStore) { + + fun export(walletAddress: String, backupPassword: String?): Single { + return passwordStore.getPassword(walletAddress) + .flatMap { walletRepository.exportWallet(walletAddress, it, backupPassword) } + .observeOn(AndroidSchedulers.mainThread()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/FetchAllTokenInfoInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/FetchAllTokenInfoInteract.java deleted file mode 100644 index 8ff107b3404..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/interact/FetchAllTokenInfoInteract.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.asfoundation.wallet.interact; - -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.TokenRepositoryType; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; - -public class FetchAllTokenInfoInteract { - private final TokenRepositoryType tokenRepository; - - public FetchAllTokenInfoInteract(TokenRepositoryType tokenRepository) { - this.tokenRepository = tokenRepository; - } - - public Observable fetch(Wallet wallet) { - return tokenRepository.fetchAll(wallet.address) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/FetchCreditsInteract.kt b/app/src/main/java/com/asfoundation/wallet/interact/FetchCreditsInteract.kt new file mode 100644 index 00000000000..bd004ae4d7a --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/FetchCreditsInteract.kt @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.interact + +import io.reactivex.Single +import java.math.BigDecimal + +class FetchCreditsInteract(private val balanceGetter: BalanceGetter) { + fun getBalance(address: String): Single { + return balanceGetter.getBalance(address) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/FetchGasSettingsInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/FetchGasSettingsInteract.java deleted file mode 100644 index 83794b7b5b5..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/interact/FetchGasSettingsInteract.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.asfoundation.wallet.interact; - -import com.asfoundation.wallet.entity.GasSettings; -import com.asfoundation.wallet.repository.GasSettingsRepositoryType; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; - -public class FetchGasSettingsInteract { - private final GasSettingsRepositoryType repository; - - public FetchGasSettingsInteract(GasSettingsRepositoryType repository) { - this.repository = repository; - } - - public Single fetch(boolean forTokenTransfer) { - return repository.getGasSettings(forTokenTransfer) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/FetchGasSettingsInteract.kt b/app/src/main/java/com/asfoundation/wallet/interact/FetchGasSettingsInteract.kt new file mode 100644 index 00000000000..730b1b0e511 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/FetchGasSettingsInteract.kt @@ -0,0 +1,18 @@ +package com.asfoundation.wallet.interact + +import com.asfoundation.wallet.entity.GasSettings +import com.asfoundation.wallet.repository.GasSettingsRepositoryType +import io.reactivex.Scheduler +import io.reactivex.Single + +class FetchGasSettingsInteract(private val repository: GasSettingsRepositoryType, + private val networkScheduler: Scheduler, + private val vieScheduler: Scheduler) { + + fun fetch(forTokenTransfer: Boolean): Single { + return repository.getGasSettings(forTokenTransfer) + .subscribeOn(networkScheduler) + .observeOn(vieScheduler) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/FetchTokensInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/FetchTokensInteract.java deleted file mode 100644 index 6a01b455e90..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/interact/FetchTokensInteract.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.asfoundation.wallet.interact; - -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.TokenRepositoryType; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.Arrays; - -public class FetchTokensInteract { - - private final TokenRepositoryType tokenRepository; - private final DefaultTokenProvider defaultTokenProvider; - - public FetchTokensInteract(TokenRepositoryType tokenRepository, - DefaultTokenProvider defaultTokenProvider) { - this.tokenRepository = tokenRepository; - this.defaultTokenProvider = defaultTokenProvider; - } - - public Observable fetch(Wallet wallet) { - return tokenRepository.fetchActive(wallet.address) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } - - public Observable fetchDefaultToken(Wallet wallet) { - return tokenRepository.fetchActive(wallet.address) - .flatMap(tokens -> defaultTokenProvider.getDefaultToken() - .flatMapObservable(defaultToken -> Observable.fromIterable(Arrays.asList(tokens)) - .filter(token -> token.tokenInfo.address.equalsIgnoreCase(defaultToken.address)))) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/FetchTransactionsInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/FetchTransactionsInteract.java index 4928d9c9f0d..68878546b50 100644 --- a/app/src/main/java/com/asfoundation/wallet/interact/FetchTransactionsInteract.java +++ b/app/src/main/java/com/asfoundation/wallet/interact/FetchTransactionsInteract.java @@ -1,11 +1,11 @@ package com.asfoundation.wallet.interact; -import com.asfoundation.wallet.entity.RawTransaction; -import com.asfoundation.wallet.entity.Wallet; import com.asfoundation.wallet.repository.TransactionRepositoryType; +import com.asfoundation.wallet.transactions.Transaction; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; +import java.util.List; public class FetchTransactionsInteract { @@ -15,9 +15,13 @@ public FetchTransactionsInteract(TransactionRepositoryType transactionRepository this.transactionRepository = transactionRepository; } - public Observable fetch(Wallet wallet) { + public Observable> fetch(String wallet) { return transactionRepository.fetchTransaction(wallet) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } + + public void stop() { + transactionRepository.stop(); + } } diff --git a/app/src/main/java/com/asfoundation/wallet/interact/FetchWalletsInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/FetchWalletsInteract.java index e5a9000681d..ec157eb4045 100644 --- a/app/src/main/java/com/asfoundation/wallet/interact/FetchWalletsInteract.java +++ b/app/src/main/java/com/asfoundation/wallet/interact/FetchWalletsInteract.java @@ -3,7 +3,6 @@ import com.asfoundation.wallet.entity.Wallet; import com.asfoundation.wallet.repository.WalletRepositoryType; import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; public class FetchWalletsInteract { @@ -14,7 +13,6 @@ public FetchWalletsInteract(WalletRepositoryType accountRepository) { } public Single fetch() { - return accountRepository.fetchWallets() - .observeOn(AndroidSchedulers.mainThread()); + return accountRepository.fetchWallets(); } } diff --git a/app/src/main/java/com/asfoundation/wallet/interact/FindDefaultNetworkInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/FindDefaultNetworkInteract.java index 6c3e3de82c6..9edf325eea5 100644 --- a/app/src/main/java/com/asfoundation/wallet/interact/FindDefaultNetworkInteract.java +++ b/app/src/main/java/com/asfoundation/wallet/interact/FindDefaultNetworkInteract.java @@ -1,23 +1,21 @@ package com.asfoundation.wallet.interact; import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; import io.reactivex.Scheduler; import io.reactivex.Single; public class FindDefaultNetworkInteract { - private final EthereumNetworkRepositoryType ethereumNetworkRepository; + private final NetworkInfo defaultNetwork; private final Scheduler scheduler; - public FindDefaultNetworkInteract(EthereumNetworkRepositoryType ethereumNetworkRepository, - Scheduler scheduler) { - this.ethereumNetworkRepository = ethereumNetworkRepository; + public FindDefaultNetworkInteract(NetworkInfo defaultNetwork, Scheduler scheduler) { + this.defaultNetwork = defaultNetwork; this.scheduler = scheduler; } public Single find() { - return Single.just(ethereumNetworkRepository.getDefaultNetwork()) + return Single.just(defaultNetwork) .observeOn(scheduler); } } diff --git a/app/src/main/java/com/asfoundation/wallet/interact/FindDefaultWalletInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/FindDefaultWalletInteract.java deleted file mode 100644 index 41bb8bef5cf..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/interact/FindDefaultWalletInteract.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.asfoundation.wallet.interact; - -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; - -public class FindDefaultWalletInteract { - - private final WalletRepositoryType walletRepository; - - public FindDefaultWalletInteract(WalletRepositoryType walletRepository) { - this.walletRepository = walletRepository; - } - - public Single find() { - return walletRepository.getDefaultWallet() - .onErrorResumeNext(throwable -> walletRepository.fetchWallets() - .filter(wallets -> wallets.length > 0) - .map(wallets -> wallets[0]) - .flatMapCompletable(walletRepository::setDefaultWallet) - .andThen(walletRepository.getDefaultWallet())) - .observeOn(AndroidSchedulers.mainThread()); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/FindDefaultWalletInteract.kt b/app/src/main/java/com/asfoundation/wallet/interact/FindDefaultWalletInteract.kt new file mode 100644 index 00000000000..f6ae0096cd1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/FindDefaultWalletInteract.kt @@ -0,0 +1,25 @@ +package com.asfoundation.wallet.interact + +import com.asfoundation.wallet.entity.Wallet +import com.asfoundation.wallet.repository.WalletRepositoryType +import io.reactivex.Scheduler +import io.reactivex.Single + +class FindDefaultWalletInteract(private val walletRepository: WalletRepositoryType, + private val scheduler: Scheduler) { + fun find(): Single { + return walletRepository.defaultWallet.subscribeOn(scheduler) + .onErrorResumeNext { + walletRepository.fetchWallets() + .filter { wallets: Array -> wallets.isNotEmpty() } + .map { wallets: Array -> + wallets[0] + } + .flatMapCompletable { wallet: Wallet -> + walletRepository.setDefaultWallet(wallet.address) + } + .andThen(walletRepository.defaultWallet) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/GetDefaultWalletBalance.java b/app/src/main/java/com/asfoundation/wallet/interact/GetDefaultWalletBalance.java deleted file mode 100644 index e640e0e43dd..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/interact/GetDefaultWalletBalance.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.asfoundation.wallet.interact; - -import com.asfoundation.wallet.entity.GasSettings; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.TransactionBuilder; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.BalanceService; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import com.asfoundation.wallet.util.UnknownTokenException; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.HashMap; -import java.util.Map; - -import static com.asfoundation.wallet.util.BalanceUtils.weiToEth; - -public class GetDefaultWalletBalance implements BalanceService { - - private final WalletRepositoryType walletRepository; - private final EthereumNetworkRepositoryType ethereumNetworkRepository; - private final FetchTokensInteract fetchTokensInteract; - private final FindDefaultWalletInteract defaultWalletInteract; - - public GetDefaultWalletBalance(WalletRepositoryType walletRepository, - EthereumNetworkRepositoryType ethereumNetworkRepository, - FetchTokensInteract fetchTokensInteract, FindDefaultWalletInteract defaultWalletInteract) { - this.walletRepository = walletRepository; - this.ethereumNetworkRepository = ethereumNetworkRepository; - this.fetchTokensInteract = fetchTokensInteract; - this.defaultWalletInteract = defaultWalletInteract; - } - - public Single> get(Wallet wallet) { - return fetchTokensInteract.fetchDefaultToken(wallet) - .flatMapSingle(token -> { - if (wallet.address.equals(token.tokenInfo.address)) { - return getEtherBalance(wallet); - } else { - return getTokenBalance(token); - } - }) - .firstOrError(); - } - - private Single> getTokenBalance(Token token) { - Map balance = new HashMap<>(); - balance.put(token.tokenInfo.symbol, - weiToEth(token.balance).setScale(4, RoundingMode.HALF_UP) - .stripTrailingZeros() - .toPlainString()); - return Single.just(balance); - } - - private Single> getEtherBalance(Wallet wallet) { - return walletRepository.balanceInWei(wallet) - .flatMap(ethBalance -> { - Map balance = new HashMap<>(); - balance.put(ethereumNetworkRepository.getDefaultNetwork().symbol, - weiToEth(ethBalance).setScale(4, RoundingMode.HALF_UP) - .stripTrailingZeros() - .toPlainString()); - return Single.just(balance); - }) - .observeOn(AndroidSchedulers.mainThread()); - } - - @Override public Single hasEnoughBalance(TransactionBuilder transactionBuilder, - BigDecimal transactionGasLimit) { - GasSettings gasSettings = transactionBuilder.gasSettings(); - return Single.zip(hasEnoughForFee(gasSettings.gasPrice.multiply(transactionGasLimit)), - hasEnoughForTransfer(transactionBuilder.amount(), transactionBuilder.shouldSendToken(), - gasSettings.gasPrice.multiply(transactionGasLimit), - transactionBuilder.contractAddress()), this::mapToState); - } - - public BalanceState mapToState(Boolean enoughEther, boolean enoughTokens) { - if (enoughTokens && enoughEther) { - return BalanceState.OK; - } else if (!enoughTokens && !enoughEther) { - return BalanceState.NO_ETHER_NO_TOKEN; - } else if (enoughEther) { - return BalanceState.NO_TOKEN; - } else { - return BalanceState.NO_ETHER; - } - } - - private Single hasEnoughForFee(BigDecimal cost) { - return getBalanceInWei().map(ethBalance -> ethBalance.compareTo(cost) >= 0); - } - - private Single getBalanceInWei() { - return defaultWalletInteract.find() - .flatMap(walletRepository::balanceInWei); - } - - private Single hasEnoughForTransfer(BigDecimal cost, boolean isTokenTransfer, - BigDecimal feeCost, String contractAddress) { - if (isTokenTransfer) { - return getToken(contractAddress).map(token -> token.balance.compareTo(cost) >= 0); - } - return getBalanceInWei().map(ethBalance -> ethBalance.subtract(feeCost) - .compareTo(cost) >= 0); - } - - private Single getToken(String contractAddress) { - return defaultWalletInteract.find() - .flatMapObservable(fetchTokensInteract::fetch) - .firstOrError() - .flatMap(tokens -> { - for (Token token : tokens) { - if (token.tokenInfo.address.equalsIgnoreCase(contractAddress)) { - return Single.just(token); - } - } - return Single.error(new UnknownTokenException()); - }); - } - - public enum BalanceState { - NO_TOKEN, NO_ETHER, NO_ETHER_NO_TOKEN, OK - } -} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/GetDefaultWalletBalanceInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/GetDefaultWalletBalanceInteract.java new file mode 100644 index 00000000000..c0be3ad899d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/GetDefaultWalletBalanceInteract.java @@ -0,0 +1,141 @@ +package com.asfoundation.wallet.interact; + +import com.asfoundation.wallet.entity.Balance; +import com.asfoundation.wallet.entity.GasSettings; +import com.asfoundation.wallet.entity.NetworkInfo; +import com.asfoundation.wallet.entity.Token; +import com.asfoundation.wallet.entity.TokenInfo; +import com.asfoundation.wallet.entity.TransactionBuilder; +import com.asfoundation.wallet.entity.Wallet; +import com.asfoundation.wallet.repository.BalanceService; +import com.asfoundation.wallet.repository.TokenRepositoryType; +import com.asfoundation.wallet.repository.WalletRepositoryType; +import com.asfoundation.wallet.util.UnknownTokenException; +import io.reactivex.Single; +import java.math.BigDecimal; +import java.math.RoundingMode; + +import static com.asfoundation.wallet.util.BalanceUtils.weiToEth; + +public class GetDefaultWalletBalanceInteract implements BalanceService { + private final WalletRepositoryType walletRepository; + private final FindDefaultWalletInteract defaultWalletInteract; + private final FetchCreditsInteract fetchCreditsInteract; + private final NetworkInfo defaultNetwork; + private final TokenRepositoryType tokenRepositoryType; + + public GetDefaultWalletBalanceInteract(WalletRepositoryType walletRepository, + FindDefaultWalletInteract defaultWalletInteract, FetchCreditsInteract fetchCreditsInteract, + NetworkInfo defaultNetwork, TokenRepositoryType tokenRepositoryType) { + this.walletRepository = walletRepository; + this.defaultWalletInteract = defaultWalletInteract; + this.fetchCreditsInteract = fetchCreditsInteract; + this.defaultNetwork = defaultNetwork; + this.tokenRepositoryType = tokenRepositoryType; + } + + public Single getAppcBalance(String address) { + return tokenRepositoryType.getAppcBalance(address) + .flatMap(this::getAppcBalance); + } + + private Single getAppcToken(String address) { + return tokenRepositoryType.getAppcBalance(address); + } + + public Single getEthereumBalance(String address) { + return getEtherBalance(address); + } + + public Single getCredits(String address) { + return fetchCreditsInteract.getBalance(address) + .flatMap(this::getCreditsBalance); + } + + private Single getAppcBalance(Token token) { + return Single.just(new Balance(token.tokenInfo.symbol.toUpperCase(), + weiToEth(token.balance).setScale(4, RoundingMode.FLOOR))); + } + + private Single getCreditsBalance(BigDecimal value) { + return Single.just(new Balance("APPC-C", weiToEth(value).setScale(4, RoundingMode.FLOOR))); + } + + private Single getEtherBalance(String address) { + return walletRepository.getEthBalanceInWei(address) + .flatMap(ethBalance -> Single.just(new Balance(defaultNetwork.symbol, + weiToEth(ethBalance).setScale(4, RoundingMode.FLOOR)))); + } + + @Override public Single hasEnoughBalance(TransactionBuilder transactionBuilder, + BigDecimal transactionGasLimit) { + GasSettings gasSettings = transactionBuilder.gasSettings(); + return Single.zip(hasEnoughForFee(gasSettings.gasPrice.multiply(transactionGasLimit)), + hasEnoughForTransfer(transactionBuilder.amount(), transactionBuilder.shouldSendToken(), + gasSettings.gasPrice.multiply(transactionGasLimit), + transactionBuilder.contractAddress()), this::mapToState); + } + + private BalanceState mapToState(Boolean enoughEther, boolean enoughTokens) { + if (enoughTokens && enoughEther) { + return BalanceState.OK; + } else if (!enoughTokens && !enoughEther) { + return BalanceState.NO_ETHER_NO_TOKEN; + } else if (enoughEther) { + return BalanceState.NO_TOKEN; + } else { + return BalanceState.NO_ETHER; + } + } + + private Single hasEnoughForFee(BigDecimal cost) { + return getBalanceInWei().map(ethBalance -> ethBalance.compareTo(cost) >= 0); + } + + private Single getBalanceInWei() { + return defaultWalletInteract.find() + .flatMap((Wallet wallet) -> walletRepository.getEthBalanceInWei(wallet.address)); + } + + private Single hasEnoughForTransfer(BigDecimal cost, boolean isTokenTransfer, + BigDecimal feeCost, String contractAddress) { + if (isTokenTransfer) { + return getAppcToken().flatMap(token -> { + if (token.tokenInfo.address.equalsIgnoreCase(contractAddress)) { + return Single.just(token); + } else { + return Single.error(new UnknownTokenException()); + } + }) + .map(token -> normalizeBalance(token.balance, token.tokenInfo).compareTo(cost) >= 0); + } + return getBalanceInWei().map(ethBalance -> ethBalance.subtract(feeCost) + .compareTo(cost) >= 0); + } + + private Single getAppcToken() { + return defaultWalletInteract.find() + .flatMap(wallet -> getAppcToken(wallet.address)); + } + + private BigDecimal normalizeBalance(BigDecimal balance, TokenInfo tokenInfo) { + return convertToMainMetric(balance, tokenInfo.decimals); + } + + private BigDecimal convertToMainMetric(BigDecimal value, int decimals) { + try { + StringBuilder divider = new StringBuilder(18); + divider.append("1"); + for (int i = 0; i < decimals; i++) { + divider.append("0"); + } + return value.divide(new BigDecimal(divider.toString()), decimals, RoundingMode.DOWN); + } catch (NumberFormatException ex) { + return BigDecimal.ZERO; + } + } + + public enum BalanceState { + NO_TOKEN, NO_ETHER, NO_ETHER_NO_TOKEN, OK + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/ImportWalletInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/ImportWalletInteract.java deleted file mode 100644 index bd81f09b619..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/interact/ImportWalletInteract.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.asfoundation.wallet.interact; - -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.interact.rx.operator.Operators; -import com.asfoundation.wallet.repository.PasswordStore; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; - -public class ImportWalletInteract { - - private final WalletRepositoryType walletRepository; - private final PasswordStore passwordStore; - - public ImportWalletInteract(WalletRepositoryType walletRepository, PasswordStore passwordStore) { - this.walletRepository = walletRepository; - this.passwordStore = passwordStore; - } - - public Single importKeystore(String keystore, String password) { - return passwordStore.generatePassword() - .flatMap( - newPassword -> walletRepository.importKeystoreToWallet(keystore, password, newPassword) - .compose(Operators.savePassword(passwordStore, walletRepository, newPassword))) - .observeOn(AndroidSchedulers.mainThread()); - } - - public Single importPrivateKey(String privateKey) { - return passwordStore.generatePassword() - .flatMap(newPassword -> walletRepository.importPrivateKeyToWallet(privateKey, newPassword) - .compose(Operators.savePassword(passwordStore, walletRepository, newPassword))) - .observeOn(AndroidSchedulers.mainThread()); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/RestoreWalletInteractor.kt b/app/src/main/java/com/asfoundation/wallet/interact/RestoreWalletInteractor.kt new file mode 100644 index 00000000000..af8c90b8b08 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/RestoreWalletInteractor.kt @@ -0,0 +1,78 @@ +package com.asfoundation.wallet.interact + +import android.net.Uri +import android.os.Build +import com.asfoundation.wallet.backup.FileInteractor +import com.asfoundation.wallet.interact.rx.operator.Operators +import com.asfoundation.wallet.repository.PasswordStore +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.repository.WalletRepositoryType +import com.asfoundation.wallet.util.RestoreError +import com.asfoundation.wallet.util.RestoreErrorType +import io.reactivex.Completable +import io.reactivex.Single + +class RestoreWalletInteractor(private val walletRepository: WalletRepositoryType, + private val setDefaultWalletInteract: SetDefaultWalletInteract, + private val passwordStore: PasswordStore, + private val preferencesRepositoryType: PreferencesRepositoryType, + private val fileInteractor: FileInteractor) { + + fun isKeystore(key: String) = key.contains("{") + + fun restoreKeystore(keystore: String, password: String = ""): Single { + return passwordStore.generatePassword() + .flatMap { newPassword -> + walletRepository.restoreKeystoreToWallet(keystore, password, newPassword) + .compose(Operators.savePassword(passwordStore, walletRepository, newPassword)) + } + .doOnSuccess { preferencesRepositoryType.setWalletRestoreBackup(it.address) } + .map { WalletModel(it.address) } + .onErrorReturn { mapError(keystore, it) } + } + + fun restorePrivateKey(privateKey: String?): Single { + return passwordStore.generatePassword() + .flatMap { newPassword -> + walletRepository.restorePrivateKeyToWallet(privateKey, newPassword) + .compose(Operators.savePassword(passwordStore, walletRepository, newPassword)) + } + .map { WalletModel(it.address) } + .doOnSuccess { preferencesRepositoryType.setWalletRestoreBackup(it.address) } + .onErrorReturn { WalletModel(RestoreError(RestoreErrorType.GENERIC)) } + } + + fun setDefaultWallet(address: String): Completable { + return setDefaultWalletInteract.set(address) + } + + fun getPath(): Uri? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + preferencesRepositoryType.getChosenUri() + ?.let { Uri.parse(it) } + } else { + fileInteractor.getDownloadPath() + ?.let { fileInteractor.getUriFromFile(it) } + } + } + + private fun mapError(keystore: String, throwable: Throwable): WalletModel { + if (throwable.message != null) { + if ((throwable.message as String).contains("Invalid Keystore", true)) { + return WalletModel(RestoreError(RestoreErrorType.INVALID_KEYSTORE)) + } + return when (throwable.message) { + "Invalid password provided" -> WalletModel(keystore, + RestoreError(RestoreErrorType.INVALID_PASS)) + "Already added" -> WalletModel(RestoreError(RestoreErrorType.ALREADY_ADDED)) + else -> WalletModel(RestoreError(RestoreErrorType.GENERIC)) + } + } else { + return WalletModel(RestoreError(RestoreErrorType.GENERIC)) + } + } + + fun readFile(fileUri: Uri?): Single { + return fileInteractor.readFile(fileUri) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/SendTransactionInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/SendTransactionInteract.java index 206693ca474..60669659514 100644 --- a/app/src/main/java/com/asfoundation/wallet/interact/SendTransactionInteract.java +++ b/app/src/main/java/com/asfoundation/wallet/interact/SendTransactionInteract.java @@ -1,12 +1,10 @@ package com.asfoundation.wallet.interact; import com.asfoundation.wallet.entity.TransactionBuilder; -import com.asfoundation.wallet.entity.Wallet; import com.asfoundation.wallet.repository.PasswordStore; import com.asfoundation.wallet.repository.TransactionRepositoryType; import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import java.math.BigInteger; +import io.reactivex.schedulers.Schedulers; public class SendTransactionInteract { @@ -20,18 +18,30 @@ public SendTransactionInteract(TransactionRepositoryType transactionRepository, } public Single send(TransactionBuilder transactionBuilder) { - return passwordStore.getPassword(new Wallet(transactionBuilder.fromAddress())) - .flatMap(password -> transactionRepository.createTransaction(transactionBuilder, password) - .observeOn(AndroidSchedulers.mainThread())); + return passwordStore.getPassword(transactionBuilder.fromAddress()) + .subscribeOn(Schedulers.io()) + .flatMap(password -> transactionRepository.createTransaction(transactionBuilder, password)); } - public Single approve(TransactionBuilder transactionBuilder, BigInteger nonce) { - return passwordStore.getPassword(new Wallet(transactionBuilder.fromAddress())) - .flatMap(password -> transactionRepository.approve(transactionBuilder, password, nonce)); + public Single approve(TransactionBuilder transactionBuilder) { + return passwordStore.getPassword(transactionBuilder.fromAddress()) + .flatMap(password -> transactionRepository.approve(transactionBuilder, password)); } - public Single buy(TransactionBuilder transaction, BigInteger nonce) { - return passwordStore.getPassword(new Wallet(transaction.fromAddress())) - .flatMap(password -> transactionRepository.callIab(transaction, password, nonce)); + public Single buy(TransactionBuilder transaction) { + return passwordStore.getPassword(transaction.fromAddress()) + .flatMap(password -> transactionRepository.callIab(transaction, password)); + } + + public Single computeApproveTransactionHash(TransactionBuilder transactionBuilder) { + return passwordStore.getPassword(transactionBuilder.fromAddress()) + .flatMap(password -> transactionRepository.computeApproveTransactionHash(transactionBuilder, + password)); + } + + public Single computeBuyTransactionHash(TransactionBuilder transactionBuilder) { + return passwordStore.getPassword(transactionBuilder.fromAddress()) + .flatMap(password -> transactionRepository.computeBuyTransactionHash(transactionBuilder, + password)); } } diff --git a/app/src/main/java/com/asfoundation/wallet/interact/SetDefaultWalletInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/SetDefaultWalletInteract.java index 38ed0eaf9b8..d1451261522 100644 --- a/app/src/main/java/com/asfoundation/wallet/interact/SetDefaultWalletInteract.java +++ b/app/src/main/java/com/asfoundation/wallet/interact/SetDefaultWalletInteract.java @@ -1,9 +1,7 @@ package com.asfoundation.wallet.interact; -import com.asfoundation.wallet.entity.Wallet; import com.asfoundation.wallet.repository.WalletRepositoryType; import io.reactivex.Completable; -import io.reactivex.android.schedulers.AndroidSchedulers; public class SetDefaultWalletInteract { @@ -13,8 +11,7 @@ public SetDefaultWalletInteract(WalletRepositoryType walletRepositoryType) { this.accountRepository = walletRepositoryType; } - public Completable set(Wallet wallet) { - return accountRepository.setDefaultWallet(wallet) - .observeOn(AndroidSchedulers.mainThread()); + public Completable set(String address) { + return accountRepository.setDefaultWallet(address); } } diff --git a/app/src/main/java/com/asfoundation/wallet/interact/SmsValidationInteract.kt b/app/src/main/java/com/asfoundation/wallet/interact/SmsValidationInteract.kt new file mode 100644 index 00000000000..ca03d6b9adb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/SmsValidationInteract.kt @@ -0,0 +1,41 @@ +package com.asfoundation.wallet.interact + +import com.asfoundation.wallet.entity.Wallet +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.repository.SmsValidationRepositoryType +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import io.reactivex.Single + +class SmsValidationInteract( + private val smsValidationRepository: SmsValidationRepositoryType, + private val preferencesRepositoryType: PreferencesRepositoryType +) { + + fun isValidated(address: String): Single { + return getValidationStatus(address).map { it == WalletValidationStatus.SUCCESS } + } + + fun getValidationStatus(address: String): Single { + return smsValidationRepository.isValid(address) + .doOnSuccess { saveWalletVerifiedStatus(it, address) } + } + + fun requestValidationCode(phoneNumber: String): Single { + return smsValidationRepository.requestValidationCode(phoneNumber) + } + + fun validateCode(phoneNumber: String, wallet: Wallet, + code: String): Single { + return smsValidationRepository.validateCode(phoneNumber, wallet.address, code) + .doOnSuccess { saveWalletVerifiedStatus(it, wallet.address) } + } + + private fun saveWalletVerifiedStatus(status: WalletValidationStatus, walletAddress: String) { + if (status == WalletValidationStatus.DOUBLE_SPENT || status == WalletValidationStatus.SUCCESS) { + preferencesRepositoryType.setWalletValidationStatus(walletAddress, true) + } else if (status == WalletValidationStatus.GENERIC_ERROR) { + preferencesRepositoryType.setWalletValidationStatus(walletAddress, false) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/interact/TransactionViewInteract.kt b/app/src/main/java/com/asfoundation/wallet/interact/TransactionViewInteract.kt new file mode 100644 index 00000000000..5b10f69874c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/TransactionViewInteract.kt @@ -0,0 +1,74 @@ +package com.asfoundation.wallet.interact + +import android.util.Pair +import com.appcoins.wallet.gamification.GamificationScreen +import com.appcoins.wallet.gamification.repository.Levels +import com.asfoundation.wallet.entity.Balance +import com.asfoundation.wallet.entity.NetworkInfo +import com.asfoundation.wallet.entity.Wallet +import com.asfoundation.wallet.promotions.PromotionUpdateScreen +import com.asfoundation.wallet.promotions.PromotionsInteractorContract +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.referrals.ReferralsScreen +import com.asfoundation.wallet.transactions.Transaction +import com.asfoundation.wallet.ui.balance.BalanceInteract +import com.asfoundation.wallet.ui.gamification.GamificationInteractor +import com.asfoundation.wallet.ui.iab.FiatValue +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single + +class TransactionViewInteract(private val findDefaultNetworkInteract: FindDefaultNetworkInteract, + private val findDefaultWalletInteract: FindDefaultWalletInteract, + private val fetchTransactionsInteract: FetchTransactionsInteract, + private val gamificationInteractor: GamificationInteractor, + private val balanceInteract: BalanceInteract, + private val promotionsInteractor: PromotionsInteractorContract, + private val cardNotificationsInteractor: CardNotificationsInteractor, + private val autoUpdateInteract: AutoUpdateInteract) { + + val levels: Single + get() = gamificationInteractor.getLevels() + + val appcBalance: Observable> + get() = balanceInteract.getAppcBalance() + + val ethereumBalance: Observable> + get() = balanceInteract.getEthBalance() + + val creditsBalance: Observable> + get() = balanceInteract.getCreditsBalance() + + val cardNotifications: Single> + get() = cardNotificationsInteractor.getCardNotifications() + + val userLevel: Single + get() = gamificationInteractor.getUserStats() + .map { it.level } + + fun findNetwork(): Single { + return findDefaultNetworkInteract.find() + } + + fun hasPromotionUpdate(): Single { + return promotionsInteractor.hasAnyPromotionUpdate(ReferralsScreen.PROMOTIONS, + GamificationScreen.PROMOTIONS, PromotionUpdateScreen.PROMOTIONS) + } + + fun fetchTransactions(wallet: Wallet?): Observable> { + return wallet?.let { fetchTransactionsInteract.fetch(wallet.address) } ?: Observable.just( + emptyList()) + } + + fun stopTransactionFetch() = fetchTransactionsInteract.stop() + + fun findWallet(): Single { + return findDefaultWalletInteract.find() + } + + fun dismissNotification(cardNotification: CardNotification): Completable { + return cardNotificationsInteractor.dismissNotification(cardNotification) + } + + fun retrieveUpdateIntent() = autoUpdateInteract.buildUpdateIntent() +} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/UpdateNotification.kt b/app/src/main/java/com/asfoundation/wallet/interact/UpdateNotification.kt new file mode 100644 index 00000000000..360ffd5b003 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/UpdateNotification.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.interact + +import androidx.annotation.RawRes +import androidx.annotation.StringRes +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction + +data class UpdateNotification(@StringRes override val title: Int, + @StringRes override val body: Int, @StringRes + override val positiveButtonText: Int, + override val positiveAction: CardNotificationAction, @RawRes + val animation: Int) : + CardNotification(title, body, null, positiveButtonText, positiveAction) diff --git a/app/src/main/java/com/asfoundation/wallet/interact/WalletCreatorInteract.java b/app/src/main/java/com/asfoundation/wallet/interact/WalletCreatorInteract.java new file mode 100644 index 00000000000..237392f816d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/WalletCreatorInteract.java @@ -0,0 +1,44 @@ +package com.asfoundation.wallet.interact; + +import com.asfoundation.wallet.entity.Wallet; +import com.asfoundation.wallet.interact.rx.operator.Operators; +import com.asfoundation.wallet.repository.PasswordStore; +import com.asfoundation.wallet.repository.WalletRepositoryType; +import io.reactivex.Completable; +import io.reactivex.Scheduler; +import io.reactivex.Single; + +import static com.asfoundation.wallet.interact.rx.operator.Operators.completableErrorProxy; + +public class WalletCreatorInteract { + + private final WalletRepositoryType walletRepository; + private final PasswordStore passwordStore; + + public WalletCreatorInteract(WalletRepositoryType walletRepository, PasswordStore passwordStore, Scheduler syncScheduler) { + this.walletRepository = walletRepository; + this.passwordStore = passwordStore; + } + + public Single create() { + return passwordStore.generatePassword() + .flatMap(masterPassword -> passwordStore.setBackUpPassword(masterPassword) + .andThen(walletRepository.createWallet(masterPassword) + .compose(Operators.savePassword(passwordStore, walletRepository, masterPassword)) + .flatMap(wallet -> passwordVerification(wallet, masterPassword)))); + } + + private Single passwordVerification(Wallet wallet, String masterPassword) { + return passwordStore.getPassword(wallet.address) + .flatMap(password -> walletRepository.exportWallet(wallet.address, password, password) + .flatMap(keyStore -> walletRepository.findWallet(wallet.address))) + .onErrorResumeNext( + throwable -> walletRepository.deleteWallet(wallet.address, masterPassword) + .lift(completableErrorProxy(throwable)) + .toSingle(() -> wallet)); + } + + public Completable setDefaultWallet(String address) { + return walletRepository.setDefaultWallet(address); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/WalletModel.kt b/app/src/main/java/com/asfoundation/wallet/interact/WalletModel.kt new file mode 100644 index 00000000000..4a8d7544d67 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/interact/WalletModel.kt @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.interact + +import com.asfoundation.wallet.util.RestoreError + +data class WalletModel(val address: String, val keystore: String = "", + val error: RestoreError = RestoreError()) { + + constructor(restoreError: RestoreError) : this("", "", restoreError) + constructor(keystore: String, restoreError: RestoreError) : this("", keystore, restoreError) +} diff --git a/app/src/main/java/com/asfoundation/wallet/interact/rx/operator/CompletableErrorProxyOperator.java b/app/src/main/java/com/asfoundation/wallet/interact/rx/operator/CompletableErrorProxyOperator.java index 4af28de3e52..2196c2dbc3c 100644 --- a/app/src/main/java/com/asfoundation/wallet/interact/rx/operator/CompletableErrorProxyOperator.java +++ b/app/src/main/java/com/asfoundation/wallet/interact/rx/operator/CompletableErrorProxyOperator.java @@ -12,7 +12,7 @@ public class CompletableErrorProxyOperator implements CompletableOperator { this.throwable = throwable; } - @Override public CompletableObserver apply(CompletableObserver observer) throws Exception { + @Override public CompletableObserver apply(CompletableObserver observer) { return new DisposableCompletableObserver() { @Override public void onComplete() { if (!isDisposed()) { diff --git a/app/src/main/java/com/asfoundation/wallet/interact/rx/operator/SavePasswordOperator.java b/app/src/main/java/com/asfoundation/wallet/interact/rx/operator/SavePasswordOperator.java index 420720007be..cb00344ce46 100644 --- a/app/src/main/java/com/asfoundation/wallet/interact/rx/operator/SavePasswordOperator.java +++ b/app/src/main/java/com/asfoundation/wallet/interact/rx/operator/SavePasswordOperator.java @@ -22,7 +22,7 @@ public class SavePasswordOperator implements SingleTransformer { } @Override public Single apply(Single upstream) { - return upstream.flatMap(wallet -> passwordStore.setPassword(wallet, password) + return upstream.flatMap(wallet -> passwordStore.setPassword(wallet.address, password) .onErrorResumeNext(err -> walletRepository.deleteWallet(wallet.address, password) .lift(completableErrorProxy(err))) .toSingle(() -> wallet)); diff --git a/app/src/main/java/com/asfoundation/wallet/logging/DebugReceiver.kt b/app/src/main/java/com/asfoundation/wallet/logging/DebugReceiver.kt new file mode 100644 index 00000000000..7daf59ac441 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/logging/DebugReceiver.kt @@ -0,0 +1,29 @@ +package com.asfoundation.wallet.logging + +import android.util.Log +import com.asf.wallet.BuildConfig + +class DebugReceiver : LogReceiver { + + override fun log(tag: String?, throwable: Throwable?) { + if (BuildConfig.DEBUG) { + throwable?.printStackTrace() + Log.e(tag?: "Logger", throwable?.message, throwable) + } + } + + override fun log(tag: String?, message: String?) { + if (BuildConfig.DEBUG) { + Log.e(tag?: "Logger", message) + } + + } + + override fun log(tag: String?, message: String?, throwable: Throwable?) { + if (BuildConfig.DEBUG) { + Log.e(tag?: "Logger", message, throwable) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/logging/FlurryReceiver.kt b/app/src/main/java/com/asfoundation/wallet/logging/FlurryReceiver.kt new file mode 100644 index 00000000000..14d6b29e988 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/logging/FlurryReceiver.kt @@ -0,0 +1,34 @@ +package com.asfoundation.wallet.logging + +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.logging.LogReceiver.Companion.DEFAULT_MSG +import com.asfoundation.wallet.logging.LogReceiver.Companion.DEFAULT_THROWABLE_MSG +import com.flurry.android.FlurryAgent + +class FlurryReceiver : LogReceiver { + companion object { + private const val DEFAULT_ERROR_ID = "ID" + } + override fun log(tag: String?, throwable: Throwable?) { + throwable?.let { + throwable.printStackTrace() + if (!BuildConfig.DEBUG) { + FlurryAgent.onError(tag ?: DEFAULT_ERROR_ID, throwable.message ?: DEFAULT_THROWABLE_MSG, throwable) + } + } + } + + override fun log(tag: String?, message: String?) { + message?.let { + if (!BuildConfig.DEBUG) { + FlurryAgent.onError(tag ?: DEFAULT_ERROR_ID, message, Throwable()) + } + } + } + + override fun log(tag: String?, message: String?, throwable: Throwable?) { + if (!BuildConfig.DEBUG) { + throwable?.let { FlurryAgent.onError(tag ?: DEFAULT_ERROR_ID, message ?: DEFAULT_MSG, it) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/logging/LogReceiver.kt b/app/src/main/java/com/asfoundation/wallet/logging/LogReceiver.kt new file mode 100644 index 00000000000..9a290691e62 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/logging/LogReceiver.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.logging + +interface LogReceiver { + companion object { + const val DEFAULT_TAG = "default_tag" + const val DEFAULT_MSG = "default_message" + const val DEFAULT_THROWABLE_MSG = "default_throwable_msg" + const val DEFAULT_THROWABLE_STATCKTRACE = "default_throwable_stacktrace" + } + fun log(tag: String?, throwable: Throwable?) + fun log(tag: String?, message: String?) + fun log(tag: String?, message: String?, throwable: Throwable?) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/logging/Logger.kt b/app/src/main/java/com/asfoundation/wallet/logging/Logger.kt new file mode 100644 index 00000000000..dd496bf966a --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/logging/Logger.kt @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.logging + +interface Logger { + fun log(tag: String?, message: String?) + fun log(tag: String?, throwable: Throwable?) + fun log(tag: String?, message: String?, throwable: Throwable?) + fun addReceiver(receiver: LogReceiver) + fun removeReceiver(receiver: LogReceiver) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/logging/RakamReceiver.kt b/app/src/main/java/com/asfoundation/wallet/logging/RakamReceiver.kt new file mode 100644 index 00000000000..600bb325e46 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/logging/RakamReceiver.kt @@ -0,0 +1,70 @@ +package com.asfoundation.wallet.logging + +import com.asfoundation.wallet.logging.LogReceiver.Companion.DEFAULT_MSG +import com.asfoundation.wallet.logging.LogReceiver.Companion.DEFAULT_TAG +import com.asfoundation.wallet.logging.LogReceiver.Companion.DEFAULT_THROWABLE_STATCKTRACE +import io.rakam.api.Rakam +import org.json.JSONException +import org.json.JSONObject + +class RakamReceiver : LogReceiver { + companion object { + private const val LOG_EVENT_TYPE = "wallet_non_fatal_event" + } + + override fun log(tag: String?, throwable: Throwable?) { + Rakam.getInstance() + .logEvent(LOG_EVENT_TYPE, map(tag, null, throwable)) + } + + override fun log(tag: String?, message: String?) { + Rakam.getInstance() + .logEvent(LOG_EVENT_TYPE, map(tag, message, null)) + } + + override fun log(tag: String?, message: String?, throwable: Throwable?) { + Rakam.getInstance() + .logEvent(LOG_EVENT_TYPE, map(tag, message, throwable)) + } + + private fun map(tag: String?, message: String?, throwable: Throwable?): JSONObject { + val properties = JSONObject() + try { + properties.put("tag", tag ?: DEFAULT_TAG) + message?.let { + properties.put("message", message) + } + throwable?.let { + properties.put("throwable_message", it.message ?: DEFAULT_MSG) + properties.put("throwable_stacktrace", getStacktrace(it.stackTrace)) + } + } catch (e: JSONException) { + e.printStackTrace() + } + return properties + } + + private fun getStacktrace(stackTrace: Array?): String { + return if (stackTrace != null) { + //This is done because rakam has a limit of characters and if we pass the entire stacktrace + // the critical information doesn't show up + buildStackTraceString(stackTrace) + } else { + DEFAULT_THROWABLE_STATCKTRACE + } + } + + private fun buildStackTraceString(stackTrace: Array): String { + var firstTraceString = "" + var secondTraceString = "" + if (stackTrace.isNotEmpty()) { + firstTraceString = + "M:${stackTrace[0].methodName},L:${stackTrace[0].lineNumber}" + } + if (stackTrace.size > 1) { + secondTraceString = + "C:${stackTrace[1].className},M:${stackTrace[1].methodName},L:${stackTrace[1].lineNumber}" + } + return "$firstTraceString / $secondTraceString" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/logging/SentryReceiver.kt b/app/src/main/java/com/asfoundation/wallet/logging/SentryReceiver.kt new file mode 100644 index 00000000000..7a2fc0c7ee9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/logging/SentryReceiver.kt @@ -0,0 +1,25 @@ +package com.asfoundation.wallet.logging + +import io.sentry.Sentry + +class SentryReceiver : LogReceiver { + + override fun log(tag: String?, throwable: Throwable?) { + throwable?.let { + Sentry.capture(throwable) + } + } + + override fun log(tag: String?, message: String?) { + message?.let { + Sentry.capture("$tag: $message") + } + } + + override fun log(tag: String?, message: String?, throwable: Throwable?) { + throwable?.let { + Sentry.capture("$tag: $message") + Sentry.capture(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/logging/WalletLogger.kt b/app/src/main/java/com/asfoundation/wallet/logging/WalletLogger.kt new file mode 100644 index 00000000000..3120327fda2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/logging/WalletLogger.kt @@ -0,0 +1,23 @@ +package com.asfoundation.wallet.logging + +class WalletLogger(private var logReceivers: ArrayList): Logger { + + override fun log(tag: String?, message: String?) { + logReceivers.forEach { it.log(tag, message) } + } + + override fun log(tag: String?, throwable: Throwable?) { + logReceivers.forEach { it.log(tag, throwable) } + } + override fun log(tag: String?, message: String?, throwable: Throwable?) { + logReceivers.forEach { it.log(tag, message, throwable) } + } + + override fun addReceiver(receiver: LogReceiver) { + logReceivers.add(receiver) + } + + override fun removeReceiver(receiver: LogReceiver) { + logReceivers.remove(receiver) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/navigator/TransactionViewNavigator.kt b/app/src/main/java/com/asfoundation/wallet/navigator/TransactionViewNavigator.kt new file mode 100644 index 00000000000..c120bf14b5b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/navigator/TransactionViewNavigator.kt @@ -0,0 +1,52 @@ +package com.asfoundation.wallet.navigator + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.asfoundation.wallet.entity.Wallet +import com.asfoundation.wallet.promotions.PromotionsActivity +import com.asfoundation.wallet.router.* +import com.asfoundation.wallet.transactions.Transaction +import com.asfoundation.wallet.ui.backup.WalletBackupActivity + +class TransactionViewNavigator(private val settingsRouter: SettingsRouter, + private val sendRouter: SendRouter, + private val transactionDetailRouter: TransactionDetailRouter, + private val myAddressRouter: MyAddressRouter, + private val balanceRouter: BalanceRouter, + private val externalBrowserRouter: ExternalBrowserRouter, + private val topUpRouter: TopUpRouter) { + + fun openSettings(context: Context) = settingsRouter.open(context) + + fun openSendView(context: Context) = sendRouter.open(context) + + fun openTransactionsDetailView(context: Context, transaction: Transaction) = + transactionDetailRouter.open(context, transaction) + + fun openMyAddressView(context: Context, value: Wallet?) = myAddressRouter.open(context, value) + + fun openTokensView(context: Context) = balanceRouter.open(context) + + fun navigateToBrowser(context: Context, uri: Uri) = externalBrowserRouter.open(context, uri) + + fun openIntent(context: Context, intent: Intent) = context.startActivity(intent) + + fun openTopUp(context: Context) = topUpRouter.open(context) + + fun openPromotions(context: Context) { + val intent = Intent(context, PromotionsActivity::class.java) + .apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP } + context.startActivity(intent) + } + + fun navigateToBackup(context: Context, walletAddress: String) { + val intent = WalletBackupActivity.newIntent(context, walletAddress) + .apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } + context.startActivity(intent) + } +} + + diff --git a/app/src/main/java/com/asfoundation/wallet/navigator/UriNavigator.java b/app/src/main/java/com/asfoundation/wallet/navigator/UriNavigator.java new file mode 100644 index 00000000000..1ac93508fbe --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/navigator/UriNavigator.java @@ -0,0 +1,11 @@ +package com.asfoundation.wallet.navigator; + +import android.net.Uri; +import io.reactivex.Observable; + +public interface UriNavigator { + + void navigateToUri(String url); + + Observable uriResults(); +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/AndroidAppDataProvider.kt b/app/src/main/java/com/asfoundation/wallet/permissions/AndroidAppDataProvider.kt new file mode 100644 index 00000000000..489f15b07fa --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/AndroidAppDataProvider.kt @@ -0,0 +1,18 @@ +package com.asfoundation.wallet.permissions + +import android.content.Context +import android.graphics.drawable.Drawable + +class AndroidAppDataProvider(private val context: Context) { + fun getAppInfo(packageName: String): ApplicationInfo { + val packageInfo = context.packageManager.getApplicationInfo(packageName, 0) + val appName = context.packageManager.getApplicationLabel(packageInfo) + val icon = context.packageManager + .getApplicationIcon(packageName) + return ApplicationInfo(packageName, appName, icon) + } + + data class ApplicationInfo(val packageName: String, val appName: CharSequence, + val icon: Drawable) + +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/Permission.kt b/app/src/main/java/com/asfoundation/wallet/permissions/Permission.kt new file mode 100644 index 00000000000..4f02c4c457a --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/Permission.kt @@ -0,0 +1,3 @@ +package com.asfoundation.wallet.permissions + +data class Permission(val walletAddress: String, val permissionGranted: Boolean) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/PermissionsInteractor.kt b/app/src/main/java/com/asfoundation/wallet/permissions/PermissionsInteractor.kt new file mode 100644 index 00000000000..11a07c25ed7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/PermissionsInteractor.kt @@ -0,0 +1,51 @@ +package com.asfoundation.wallet.permissions + +import com.appcoins.wallet.permissions.ApplicationPermission +import com.appcoins.wallet.permissions.PermissionName +import com.appcoins.wallet.permissions.Permissions +import com.asfoundation.wallet.entity.Wallet +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single + +class PermissionsInteractor(private val permissions: Permissions, + private val walletInteract: FindDefaultWalletInteract) { + fun grantPermission(packageName: String, apkSignature: String, + permissionName: PermissionName): Single { + return walletInteract.find().flatMap { + Completable.fromAction { + permissions.grantPermission(it.address, packageName, apkSignature, permissionName) + } + .andThen(Single.just(it.address)) + } + } + + fun hasPermission(packageName: String, apkSignature: String, + permission: PermissionName): Single { + return walletInteract.find() + .flatMap { wallet -> + Single.just(permissions.getPermissions(wallet.address, packageName, apkSignature)).map { + for (permissionName in it) { + if (permissionName == permission) { + return@map Permission(wallet.address, true) + } + } + return@map Permission(wallet.address, false) + } + } + } + + fun getWalletAddress(): Single { + return walletInteract.find().map { it.address } + } + + fun getPermissions(): Observable> { + return getWalletAddress().flatMapObservable { permissions.getPermissions(it) } + } + + fun revokePermission(packageName: String, permissionName: PermissionName): Single { + return walletInteract.find() + .doOnSuccess { permissions.revokePermission(it.address, packageName, permissionName) } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ApplicationPermissionViewData.kt b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ApplicationPermissionViewData.kt new file mode 100644 index 00000000000..68a7d4bb113 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ApplicationPermissionViewData.kt @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.permissions.manage.view + +import android.graphics.drawable.Drawable + +data class ApplicationPermissionViewData(val packageName: String, + val appName: String, + val hasPermission: Boolean, + val icon: Drawable, + val apkSignature: String) diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ManagePermissionsActivity.kt b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ManagePermissionsActivity.kt new file mode 100644 index 00000000000..585545c7b69 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ManagePermissionsActivity.kt @@ -0,0 +1,35 @@ +package com.asfoundation.wallet.permissions.manage.view + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.asf.wallet.R +import com.asfoundation.wallet.ui.BaseActivity + +class ManagePermissionsActivity : BaseActivity(), ManagePermissionsView, ToolbarManager { + companion object { + @JvmStatic + fun newIntent(context: Context): Intent { + return Intent(context, ManagePermissionsActivity::class.java) + } + } + + override fun setupToolbar() { + setTitle(R.string.permissions_title) + toolbar() + } + + private lateinit var presenter: ManagePermissionsPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_permissions_layout) + presenter = ManagePermissionsPresenter(this) + presenter.present(savedInstanceState == null) + } + + override fun showPermissionsList() { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, PermissionsListFragment.newInstance()).commit() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ManagePermissionsPresenter.kt b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ManagePermissionsPresenter.kt new file mode 100644 index 00000000000..1044b38e92c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ManagePermissionsPresenter.kt @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.permissions.manage.view + +class ManagePermissionsPresenter(private val view: ManagePermissionsView) { + fun present(isCreating: Boolean) { + if (isCreating) { + view.showPermissionsList() + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ManagePermissionsView.kt b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ManagePermissionsView.kt new file mode 100644 index 00000000000..ab056577884 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ManagePermissionsView.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.permissions.manage.view + +interface ManagePermissionsView { + fun showPermissionsList() + +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionViewHolder.kt new file mode 100644 index 00000000000..92987016dc7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionViewHolder.kt @@ -0,0 +1,34 @@ +package com.asfoundation.wallet.permissions.manage.view + +import androidx.recyclerview.widget.RecyclerView +import android.view.View +import android.widget.ImageView +import android.widget.Switch +import android.widget.TextView +import com.asf.wallet.R +import com.jakewharton.rxrelay2.BehaviorRelay + +class PermissionViewHolder(itemView: View, + private val permissionClick: BehaviorRelay) : + RecyclerView.ViewHolder(itemView) { + private val appIcon: ImageView = itemView.findViewById(R.id.app_icon) + private val appNameTextView: TextView = itemView.findViewById(R.id.permission_app_name) + private val hasPermission: Switch = itemView.findViewById(R.id.has_permission) + + fun bindPermission(permission: ApplicationPermissionViewData) { + hasPermission.setOnCheckedChangeListener(null) + itemView.setOnClickListener(null) + appIcon.setImageDrawable(permission.icon) + appNameTextView.text = permission.appName + hasPermission.isChecked = permission.hasPermission + itemView.setOnClickListener { + hasPermission.isChecked = !hasPermission.isChecked + } + hasPermission.setOnCheckedChangeListener { _, isChecked -> + permissionClick.accept( + ApplicationPermissionViewData(permission.packageName, permission.appName, isChecked, + permission.icon, + permission.apkSignature)) + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionsListAdapter.kt b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionsListAdapter.kt new file mode 100644 index 00000000000..115d6512016 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionsListAdapter.kt @@ -0,0 +1,59 @@ +package com.asfoundation.wallet.permissions.manage.view + +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import android.view.LayoutInflater +import android.view.ViewGroup +import com.asf.wallet.R +import com.jakewharton.rxrelay2.BehaviorRelay + +class PermissionsListAdapter( + private var permissions: MutableList, + private val permissionClick: BehaviorRelay) : + RecyclerView.Adapter() { + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PermissionViewHolder { + return PermissionViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_permission_application, parent, false), permissionClick) + } + + override fun getItemCount(): Int { + return permissions.size + } + + override fun onBindViewHolder(holder: PermissionViewHolder, position: Int) { + holder.bindPermission(permissions[position]) + } + + fun setPermissions(permissions: List) { + val oldList = this.permissions + this.permissions = permissions.toMutableList() + notifyChanges(oldList, this.permissions) + } + + private fun notifyChanges(oldList: List, + newList: List) { + + DiffUtil.calculateDiff(object : DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition].packageName == newList[newItemPosition].packageName + } + + override fun getOldListSize(): Int { + return oldList.size + } + + override fun getNewListSize(): Int { + return newList.size + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition].packageName == newList[newItemPosition].packageName + && oldList[oldItemPosition].appName == newList[newItemPosition].appName + && oldList[oldItemPosition].apkSignature == newList[newItemPosition].apkSignature + && oldList[oldItemPosition].hasPermission == newList[newItemPosition].hasPermission + } + }).dispatchUpdatesTo(this) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionsListFragment.kt b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionsListFragment.kt new file mode 100644 index 00000000000..91c7cb312c7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionsListFragment.kt @@ -0,0 +1,112 @@ +package com.asfoundation.wallet.permissions.manage.view + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.appcoins.wallet.permissions.ApplicationPermission +import com.appcoins.wallet.permissions.PermissionName +import com.asf.wallet.R +import com.asfoundation.wallet.permissions.AndroidAppDataProvider +import com.asfoundation.wallet.permissions.PermissionsInteractor +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.jakewharton.rxrelay2.BehaviorRelay +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.fragment_permissions_list_layout.* +import javax.inject.Inject + +class PermissionsListFragment : BasePageViewFragment(), PermissionsListView { + companion object { + fun newInstance(): Fragment { + return PermissionsListFragment() + } + } + + @Inject + lateinit var permissionsInteractor: PermissionsInteractor + private lateinit var presenter: PermissionsListPresenter + private lateinit var adapter: PermissionsListAdapter + private lateinit var appInfoProvider: AndroidAppDataProvider + private lateinit var permissionClick: BehaviorRelay + private var toolbarManager: ToolbarManager? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = + PermissionsListPresenter(this, permissionsInteractor, AndroidSchedulers.mainThread(), + Schedulers.io(), CompositeDisposable()) + permissionClick = BehaviorRelay.create() + adapter = PermissionsListAdapter(mutableListOf(), permissionClick) + appInfoProvider = AndroidAppDataProvider(context!!) + } + + override fun getPermissionClick(): Observable { + return permissionClick.map { + PermissionsListView.ApplicationPermissionToggle(it.packageName, it.hasPermission, + it.apkSignature) + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + when (context) { + is ToolbarManager -> toolbarManager = context + else -> throw IllegalArgumentException( + "${PermissionsListFragment::class} has to be attached to an activity that implements ${ToolbarManager::class}") + } + } + + override fun onDetach() { + toolbarManager = null + super.onDetach() + } + + override fun showEmptyState() { + empty_state_view.visibility = View.VISIBLE + permissions_recycler_view.visibility = View.GONE + } + + override fun showPermissions(permissions: List): Completable { + empty_state_view.visibility = View.GONE + permissions_recycler_view.visibility = View.VISIBLE + return Single.fromCallable { map(permissions) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess { adapter.setPermissions(it) } + .ignoreElement() + } + + private fun map(permissions: List): List { + return permissions.map { + val appInfo = appInfoProvider.getAppInfo(it.packageName) + ApplicationPermissionViewData(it.packageName, appInfo.appName.toString(), + it.permissions.contains(PermissionName.WALLET_ADDRESS), appInfo.icon, it.apkSignature) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_permissions_list_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + permissions_recycler_view.adapter = adapter + permissions_recycler_view.layoutManager = LinearLayoutManager(context) + toolbarManager?.setupToolbar() + presenter.present() + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionsListPresenter.kt b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionsListPresenter.kt new file mode 100644 index 00000000000..561e586bcf1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionsListPresenter.kt @@ -0,0 +1,53 @@ +package com.asfoundation.wallet.permissions.manage.view + +import com.appcoins.wallet.permissions.PermissionName +import com.asfoundation.wallet.permissions.PermissionsInteractor +import io.reactivex.Completable +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers + +class PermissionsListPresenter(private val view: PermissionsListView, + private val permissionsInteractor: PermissionsInteractor, + private val viewScheduler: Scheduler, + private val ioScheduler: Scheduler, + private val disposables: CompositeDisposable) { + fun present() { + showPermissionsList() + handlePermissionClick() + } + + private fun handlePermissionClick() { + disposables.add(view.getPermissionClick() + .observeOn(ioScheduler) + .flatMapSingle { + return@flatMapSingle if (it.hasPermission) { + permissionsInteractor.grantPermission(it.packageName, it.apkSignature, + PermissionName.WALLET_ADDRESS) + } else { + permissionsInteractor.revokePermission(it.packageName, PermissionName.WALLET_ADDRESS) + } + } + .subscribe()) + } + + private fun showPermissionsList() { + disposables.add( + permissionsInteractor.getPermissions() + .subscribeOn(Schedulers.io()) + .observeOn(viewScheduler) + .flatMapCompletable { + if (it.isEmpty()) { + Completable.fromAction { view.showEmptyState() } + } else { + view.showPermissions(it) + } + } + .subscribe()) + } + + fun stop() { + disposables.clear() + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionsListView.kt b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionsListView.kt new file mode 100644 index 00000000000..db8f1c45f80 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/PermissionsListView.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.permissions.manage.view + +import com.appcoins.wallet.permissions.ApplicationPermission +import io.reactivex.Completable +import io.reactivex.Observable + +interface PermissionsListView { + fun showPermissions(permissions: List): Completable + fun getPermissionClick(): Observable + fun showEmptyState() + + data class ApplicationPermissionToggle(val packageName: String, + val hasPermission: Boolean, + val apkSignature: String) +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ToolbarManager.kt b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ToolbarManager.kt new file mode 100644 index 00000000000..9a685b166c7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/manage/view/ToolbarManager.kt @@ -0,0 +1,5 @@ +package com.asfoundation.wallet.permissions.manage.view + +interface ToolbarManager { + fun setupToolbar() +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionEntity.kt b/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionEntity.kt new file mode 100644 index 00000000000..f4fe321b0cb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionEntity.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.permissions.repository + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.appcoins.wallet.permissions.PermissionName + +@Entity +data class PermissionEntity(@PrimaryKey @ColumnInfo(name = "key") val key: String, + @ColumnInfo(name = "wallet_address") val walletAddress: String, + @ColumnInfo(name = "package_name") val packageName: String, + @ColumnInfo(name = "apk_signature") val apkSignature: String, + @ColumnInfo(name = "permissions") val permissions: List) diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionRepository.kt b/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionRepository.kt new file mode 100644 index 00000000000..8ebe215e4e7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionRepository.kt @@ -0,0 +1,68 @@ +package com.asfoundation.wallet.permissions.repository + +import com.appcoins.wallet.commons.Repository +import com.appcoins.wallet.permissions.ApplicationPermission +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single + +class PermissionRepository(private val permissionsDao: PermissionsDao) : + Repository { + override fun save(key: String, value: ApplicationPermission): Completable { + return Completable.fromAction { save(key, value) } + } + + override fun saveSync(key: String, value: ApplicationPermission) { + permissionsDao.insert(map(key, value)) + } + + override fun getAll(): Observable> { + return permissionsDao.getAllAsFlowable() + .flatMapSingle { + Observable.fromIterable(it).map { permission -> map(permission)!! }.toList() + }.toObservable() + } + + override fun getAllSync(): List { + return permissionsDao.getAll().map { map(it)!! } + } + + override fun remove(key: String): Completable { + return Completable.fromAction { removeSync(key) } + } + + override fun removeSync(key: String) { + permissionsDao.remove(permissionsDao.getSyncPermission(key)) + } + + override fun contains(key: String): Single { + return Single.fromCallable { containsSync(key) } + } + + override fun containsSync(key: String): Boolean { + return permissionsDao.getSyncPermission(key) != null + } + + override fun get(key: String): Observable { + return permissionsDao.getPermission(key).map { map(it)!! }.toObservable() + } + + override fun getSync(key: String): ApplicationPermission? { + return map(permissionsDao.getSyncPermission(key)) + } + + private fun map(key: String, + applicationPermission: ApplicationPermission): PermissionEntity { + return PermissionEntity(key, + applicationPermission.walletAddress, + applicationPermission.packageName, + applicationPermission.apkSignature, applicationPermission.permissions) + } + + private fun map(permission: PermissionEntity?): ApplicationPermission? { + return permission?.let { + ApplicationPermission(permission.walletAddress, permission.packageName, + permission.apkSignature, permission.permissions) + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionsDao.kt b/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionsDao.kt new file mode 100644 index 00000000000..42c0487622e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionsDao.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.permissions.repository + +import androidx.room.* +import io.reactivex.Flowable + +@Dao +interface PermissionsDao { + @Query("select * from PermissionEntity where `key` like :key") + fun getSyncPermission(key: String): PermissionEntity? + + @Query("select * from PermissionEntity where `key` like :key") + fun getPermission(key: String): Flowable + + @Query("select * from PermissionEntity order by `key`") + fun getAllAsFlowable(): Flowable> + + @Query("select * from PermissionEntity order by `key`") + fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(roomPermission: PermissionEntity) + + @Delete + fun remove(roomPermission: PermissionEntity?) + +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionsDatabase.kt b/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionsDatabase.kt new file mode 100644 index 00000000000..54890cd08a2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionsDatabase.kt @@ -0,0 +1,11 @@ +package com.asfoundation.wallet.permissions.repository + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters + +@Database(entities = [PermissionEntity::class], version = 1) +@TypeConverters(PermissionsListTypeConverter::class) +abstract class PermissionsDatabase : RoomDatabase() { + abstract fun permissionsDao(): PermissionsDao +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionsListTypeConverter.kt b/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionsListTypeConverter.kt new file mode 100644 index 00000000000..dc3fbefef60 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/repository/PermissionsListTypeConverter.kt @@ -0,0 +1,25 @@ +package com.asfoundation.wallet.permissions.repository + +import androidx.room.TypeConverter +import com.appcoins.wallet.permissions.PermissionName +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import java.util.* + + +class PermissionsListTypeConverter { + private val gson: Gson = Gson() + @TypeConverter + fun stringToPermissionsList(data: String?): List { + data?.let { + val listType = object : TypeToken>() { + }.type + return gson.fromJson(data, listType) + } ?: return Collections.emptyList() + } + + @TypeConverter + fun permissionsListToString(permissions: List): String { + return gson.toJson(permissions) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/request/view/CreateWalletFragment.kt b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/CreateWalletFragment.kt new file mode 100644 index 00000000000..565931a5bc2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/CreateWalletFragment.kt @@ -0,0 +1,94 @@ +package com.asfoundation.wallet.permissions.request.view + +import android.animation.Animator +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.interact.WalletCreatorInteract +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxrelay2.BehaviorRelay +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.fragment_create_wallet_layout.* +import javax.inject.Inject + +class CreateWalletFragment : BasePageViewFragment(), CreateWalletView { + companion object { + fun newInstance() = CreateWalletFragment() + } + + @Inject + lateinit var interactor: WalletCreatorInteract + + private lateinit var presenter: CreateWalletPresenter + private lateinit var navigator: CreateWalletNavigator + private lateinit var finishAnimationFinishEvent: BehaviorRelay + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = CreateWalletPresenter(this, CompositeDisposable(), interactor, + AndroidSchedulers.mainThread()) + finishAnimationFinishEvent = BehaviorRelay.create() + } + + + override fun onAttach(context: Context) { + super.onAttach(context) + when (context) { + is CreateWalletNavigator -> navigator = context + else -> throw IllegalArgumentException( + "${CreateWalletFragment::class} has to be attached to an activity that implements ${CreateWalletNavigator::class}") + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_create_wallet_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present() + } + + override fun onDestroyView() { + presenter.stop() + create_wallet_animation.removeAllAnimatorListeners() + create_wallet_animation.removeAllUpdateListeners() + create_wallet_animation.removeAllLottieOnCompositionLoadedListener() + super.onDestroyView() + } + + override fun getOnCreateWalletClick() = RxView.clicks(provide_wallet_create_wallet_button) + + override fun getCancelClick() = RxView.clicks(provide_wallet_cancel) + + override fun closeSuccess() = navigator.closeSuccess() + + override fun showFinishAnimation() { + create_wallet_animation.setAnimation(R.raw.success_animation) + create_wallet_text.text = getText(R.string.provide_wallet_created_header) + create_wallet_animation.playAnimation() + create_wallet_animation.repeatCount = 0 + create_wallet_animation.addAnimatorListener(object : Animator.AnimatorListener { + override fun onAnimationRepeat(animation: Animator?) = Unit + override fun onAnimationEnd(animation: Animator?) = finishAnimationFinishEvent.accept(Any()) + override fun onAnimationCancel(animation: Animator?) = Unit + override fun onAnimationStart(animation: Animator?) = Unit + }) + } + + override fun getFinishAnimationFinishEvent(): BehaviorRelay = finishAnimationFinishEvent + + override fun closeCancel() = navigator.closeCancel() + + override fun showLoading() { + create_wallet_group.visibility = View.INVISIBLE + create_wallet_animation.visibility = View.VISIBLE + create_wallet_text.visibility = View.VISIBLE + create_wallet_animation.playAnimation() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/request/view/CreateWalletNavigator.kt b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/CreateWalletNavigator.kt new file mode 100644 index 00000000000..6bf6267432b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/CreateWalletNavigator.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.permissions.request.view + +interface CreateWalletNavigator { + fun closeSuccess() + fun closeCancel() +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/request/view/CreateWalletPresenter.kt b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/CreateWalletPresenter.kt new file mode 100644 index 00000000000..e38319c5997 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/CreateWalletPresenter.kt @@ -0,0 +1,43 @@ +package com.asfoundation.wallet.permissions.request.view + +import com.asfoundation.wallet.entity.Wallet +import com.asfoundation.wallet.interact.WalletCreatorInteract +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.BiFunction +import java.util.concurrent.TimeUnit + +class CreateWalletPresenter(private val view: CreateWalletView, + private val disposables: CompositeDisposable, + private val interactor: WalletCreatorInteract, + private val viewScheduler: Scheduler) { + fun present() { + handleOnCreateWalletClick() + handleOnCancelClick() + handleOnFinishAnimationFinish() + } + + private fun handleOnFinishAnimationFinish() { + disposables.add(view.getFinishAnimationFinishEvent().subscribe { view.closeSuccess() }) + } + + private fun handleOnCancelClick() { + disposables.add(view.getCancelClick().subscribe { view.closeCancel() }) + } + + private fun handleOnCreateWalletClick() { + disposables.add( + view.getOnCreateWalletClick().doOnNext { view.showLoading() } + .flatMapSingle { + Single.zip(interactor.create(), Single.timer(1, TimeUnit.SECONDS), + BiFunction { wallet: Wallet, _: Long -> wallet }) + } + .observeOn(viewScheduler) + .subscribe { view.showFinishAnimation() }) + } + + fun stop() { + disposables.clear() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/request/view/CreateWalletView.kt b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/CreateWalletView.kt new file mode 100644 index 00000000000..db9aff6add1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/CreateWalletView.kt @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.permissions.request.view + +import com.jakewharton.rxrelay2.BehaviorRelay +import io.reactivex.Observable + +interface CreateWalletView { + fun getOnCreateWalletClick(): Observable + fun closeSuccess() + fun getCancelClick(): Observable + fun closeCancel() + fun showLoading() + fun getFinishAnimationFinishEvent(): BehaviorRelay + fun showFinishAnimation() +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionFragment.kt b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionFragment.kt new file mode 100644 index 00000000000..45ea6a8281f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionFragment.kt @@ -0,0 +1,147 @@ +package com.asfoundation.wallet.permissions.request.view + +import android.content.Context +import android.graphics.Typeface.BOLD +import android.os.Bundle +import android.text.Spannable +import android.text.SpannableString +import android.text.style.StyleSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.appcoins.wallet.permissions.PermissionName +import com.asf.wallet.R +import com.asfoundation.wallet.permissions.AndroidAppDataProvider +import com.asfoundation.wallet.permissions.PermissionsInteractor +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.functions.BiFunction +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.fragment_permissions_layout.* +import kotlinx.android.synthetic.main.provide_wallet_always_allow_wallet_apps_layout.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class PermissionFragment : BasePageViewFragment(), PermissionFragmentView { + companion object { + private const val CALLING_PACKAGE = "calling_package_key" + private const val PERMISSION_KEY = "permission_key" + private const val APK_SIGNATURE_KEY = "apk_signature_key" + + fun newInstance(callingPackage: String, apkSignature: String, + permission: PermissionName): PermissionFragment { + + return PermissionFragment().apply { + arguments = Bundle().apply { + putString(CALLING_PACKAGE, callingPackage) + putString(APK_SIGNATURE_KEY, apkSignature) + putSerializable(PERMISSION_KEY, permission) + } + } + } + } + + private lateinit var appDateProvider: AndroidAppDataProvider + + @Inject + lateinit var permissionsInteractor: PermissionsInteractor + private lateinit var navigator: PermissionFragmentNavigator + private lateinit var presenter: PermissionsFragmentPresenter + private var disposable: Disposable? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val permission: PermissionName = arguments?.getSerializable(PERMISSION_KEY) as PermissionName + presenter = PermissionsFragmentPresenter(this, permissionsInteractor, + arguments?.getString(CALLING_PACKAGE)!!, permission, + arguments?.getString(APK_SIGNATURE_KEY)!!, CompositeDisposable(), + AndroidSchedulers.mainThread()) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_permissions_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + main_view.visibility = View.INVISIBLE + presenter.present() + } + + override fun showAppData(packageName: String) { + disposable?.dispose() + disposable = Single.zip(Single.timer(500, TimeUnit.MILLISECONDS), + Single.fromCallable { appDateProvider.getAppInfo(packageName) }, + BiFunction { _: Long, app: AndroidAppDataProvider.ApplicationInfo -> app }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess { app -> + provide_wallet_always_allow_app_icon.setImageDrawable(app.icon) + + val message = getString(R.string.provide_wallet_body, app.appName) + val spannedMessage = SpannableString(message) + val walletAppName = "AppCoins Wallet" + + if (message.indexOf(walletAppName) > -1) { + spannedMessage.setSpan(StyleSpan(BOLD), message.indexOf(walletAppName), + message.indexOf(walletAppName) + walletAppName.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + spannedMessage.setSpan(StyleSpan(BOLD), message.indexOf(app.appName.toString()), + message.indexOf(app.appName.toString()) + app.appName.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + provide_wallet_always_allow_body.text = spannedMessage + progress.visibility = View.GONE + main_view.visibility = View.VISIBLE + } + .subscribe() + } + + override fun showWalletAddress(wallet: String) { + provide_wallet_always_allow_app_wallet_address.text = wallet + } + + override fun getAllowButtonClick(): Observable { + return RxView.clicks(provide_wallet_always_allow_button) + } + + override fun getAllowOnceClick(): Observable { + return RxView.clicks(provide_wallet_allow_once_button) + } + + override fun getCancelClick(): Observable { + return RxView.clicks(provide_wallet_cancel) + } + + override fun closeCancel() { + navigator.closeCancel() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + when (context) { + is PermissionFragmentNavigator -> { + navigator = context + appDateProvider = AndroidAppDataProvider(context) + } + else -> throw IllegalArgumentException( + "${PermissionFragment::class} has to be attached to an activity that implements ${PermissionFragmentNavigator::class}") + } + } + + override fun closeSuccess(walletAddress: String) { + navigator.closeSuccess(walletAddress) + } + + override fun onDestroyView() { + presenter.stop() + disposable?.dispose() + super.onDestroyView() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionFragmentNavigator.kt b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionFragmentNavigator.kt new file mode 100644 index 00000000000..bae06b18ca2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionFragmentNavigator.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.permissions.request.view + +interface PermissionFragmentNavigator { + fun closeSuccess(walletAddress: String) + fun closeCancel() +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionFragmentView.kt b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionFragmentView.kt new file mode 100644 index 00000000000..1131171319e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionFragmentView.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.permissions.request.view + +import io.reactivex.Observable + +interface PermissionFragmentView { + fun getAllowButtonClick(): Observable + fun closeSuccess(walletAddress: String) + fun getAllowOnceClick(): Observable + fun getCancelClick(): Observable + fun closeCancel() + fun showAppData(packageName: String) + fun showWalletAddress(wallet: String) +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionsActivity.kt b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionsActivity.kt new file mode 100644 index 00000000000..a43decd8b48 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionsActivity.kt @@ -0,0 +1,118 @@ +package com.asfoundation.wallet.permissions.request.view + +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import androidx.fragment.app.Fragment +import com.appcoins.wallet.permissions.PermissionName +import com.asf.wallet.R +import com.asfoundation.wallet.permissions.PermissionsInteractor +import com.asfoundation.wallet.ui.BaseActivity +import com.jakewharton.rxrelay2.BehaviorRelay +import dagger.android.AndroidInjection +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import javax.inject.Inject + + +class PermissionsActivity : BaseActivity(), PermissionsActivityView, PermissionFragmentNavigator, + CreateWalletNavigator { + + companion object { + private const val PERMISSION_NAME_KEY = "PERMISSION_NAME_KEY" + } + + @Inject + lateinit var permissionsInteractor: PermissionsInteractor + private lateinit var createWalletCompleteEvent: BehaviorRelay + private var presenter: PermissionsActivityPresenter? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_permissions_layout) + AndroidInjection.inject(this) + + createWalletCompleteEvent = BehaviorRelay.create() + try { + val permissionName = getPermission() + callingPackage?.let { + presenter = + PermissionsActivityPresenter(this, permissionsInteractor, it, + getSignature(it), permissionName, CompositeDisposable(), + AndroidSchedulers.mainThread(), savedInstanceState == null) + } ?: closeError("Null calling package") + } catch (e: IllegalArgumentException) { + closeError( + "Unknown permission name. \nKnown permissions: " + PermissionName.WALLET_ADDRESS.name) + } + } + + override fun getWalletCreatedEvent(): Observable { + return createWalletCompleteEvent + } + + override fun onResume() { + super.onResume() + presenter?.present() + } + + override fun onPause() { + presenter?.stop() + super.onPause() + } + + private fun getSignature(callingPackage: String): String { + val signature = StringBuilder() + for (sig in packageManager + .getPackageInfo(callingPackage, PackageManager.GET_SIGNATURES).signatures) { + signature.append(String(sig.toByteArray())) + } + return signature.toString() + } + + override fun closeSuccess(walletAddress: String) { + val intent = Intent() + intent.putExtra("WALLET_ADDRESS", walletAddress) + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun closeCancel() { + val intent = Intent() + setResult(Activity.RESULT_CANCELED, intent) + finish() + } + + private fun closeError(message: String) { + val intent = Intent() + intent.putExtra("ERROR_MESSAGE", message) + setResult(Activity.RESULT_CANCELED, intent) + finish() + } + + @Throws(IllegalArgumentException::class) + private fun getPermission(): PermissionName { + return PermissionName.valueOf(intent.extras?.get(PERMISSION_NAME_KEY) as String) + } + + override fun showPermissionFragment(callingPackage: String, + permission: PermissionName, apkSignature: String) { + showFragment(PermissionFragment.newInstance(callingPackage, apkSignature, permission)) + } + + override fun showWalletCreation() { + showFragment(CreateWalletFragment.newInstance()) + } + + private fun showFragment(fragment: Fragment) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, fragment) + .commit() + } + + override fun closeSuccess() { + createWalletCompleteEvent.accept(Any()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionsActivityPresenter.kt b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionsActivityPresenter.kt new file mode 100644 index 00000000000..9cb1d66b571 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionsActivityPresenter.kt @@ -0,0 +1,59 @@ +package com.asfoundation.wallet.permissions.request.view + +import com.appcoins.wallet.permissions.PermissionName +import com.asfoundation.wallet.permissions.Permission +import com.asfoundation.wallet.permissions.PermissionsInteractor +import com.asfoundation.wallet.repository.WalletNotFoundException +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable + +class PermissionsActivityPresenter( + private val view: PermissionsActivityView, + private val permissionsInteractor: PermissionsInteractor, + private val callingPackage: String, + private val apkSignature: String, + private val permissionName: PermissionName, + private val disposables: CompositeDisposable, + private val viewScheduler: Scheduler, + private val isCreating: Boolean) { + fun present() { + setupUi() + handleWalletCreationFinishEvent() + } + + private fun handleWalletCreationFinishEvent() { + view.getWalletCreatedEvent() + .flatMapSingle { showPermissionsScreen() } + .subscribe() + } + + private fun setupUi() { + if (isCreating) { + disposables.add( + showPermissionsScreen().subscribe({}, { + when (it) { + is WalletNotFoundException -> view.showWalletCreation() + } + })) + } + } + + private fun showPermissionsScreen(): Single { + return permissionsInteractor.hasPermission(callingPackage, apkSignature, permissionName) + .observeOn(viewScheduler) + .doOnSuccess { permission -> + if (permission.permissionGranted) { + view.closeSuccess(permission.walletAddress) + } else { + view.showPermissionFragment(callingPackage, permissionName, apkSignature) + } + } + } + + fun stop() { + disposables.clear() + } + + +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionsActivityView.kt b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionsActivityView.kt new file mode 100644 index 00000000000..08e2fce663d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionsActivityView.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.permissions.request.view + +import com.appcoins.wallet.permissions.PermissionName +import io.reactivex.Observable + +interface PermissionsActivityView { + fun showPermissionFragment(callingPackage: String, permission: PermissionName, + apkSignature: String) + + fun closeSuccess(walletAddress: String) + fun showWalletCreation() + fun getWalletCreatedEvent(): Observable +} diff --git a/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionsFragmentPresenter.kt b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionsFragmentPresenter.kt new file mode 100644 index 00000000000..530a5537134 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/permissions/request/view/PermissionsFragmentPresenter.kt @@ -0,0 +1,61 @@ +package com.asfoundation.wallet.permissions.request.view + +import com.appcoins.wallet.permissions.PermissionName +import com.asfoundation.wallet.permissions.PermissionsInteractor +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +class PermissionsFragmentPresenter( + private val view: PermissionFragmentView, + private val permissionsInteractor: PermissionsInteractor, + private val packageName: String, + private val permissionName: PermissionName, + private val apkSignature: String, + private val disposables: CompositeDisposable, private val viewScheduler: Scheduler) { + + fun present() { + handleAllowButtonClick() + handleAllowOnceClick() + handleCancelClick() + setupUi() + } + + private fun setupUi() { + disposables.add( + permissionsInteractor.getWalletAddress() + .observeOn(viewScheduler) + .subscribe { wallet -> + view.showWalletAddress(wallet) + }) + + view.showAppData(packageName) + } + + private fun handleCancelClick() { + disposables.add( + view.getCancelClick().doOnNext { view.closeCancel() } + .subscribe()) + } + + private fun handleAllowOnceClick() { + disposables.add( + view.getAllowOnceClick().flatMapSingle { + permissionsInteractor.getWalletAddress() + .observeOn(viewScheduler) + }.doOnNext { view.closeSuccess(it) } + .subscribe()) + } + + private fun handleAllowButtonClick() { + disposables.add(view.getAllowButtonClick() + .flatMapSingle { + permissionsInteractor.grantPermission(packageName, apkSignature, permissionName) + }.observeOn(viewScheduler) + .doOnNext { view.closeSuccess(it) } + .subscribe()) + } + + fun stop() { + disposables.clear() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/poa/BackEndErrorMapper.kt b/app/src/main/java/com/asfoundation/wallet/poa/BackEndErrorMapper.kt new file mode 100644 index 00000000000..759d5a51e47 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/poa/BackEndErrorMapper.kt @@ -0,0 +1,49 @@ +package com.asfoundation.wallet.poa + +import com.asfoundation.wallet.entity.SubmitPoAException +import com.asfoundation.wallet.entity.SubmitPoAException.Companion.ALREADY_SUBMITTED_FOR_IP +import com.asfoundation.wallet.entity.SubmitPoAException.Companion.ALREADY_SUBMITTED_FOR_WALLET +import com.asfoundation.wallet.entity.SubmitPoAException.Companion.CAMPAIGN_NOT_AVAILABLE +import com.asfoundation.wallet.entity.SubmitPoAException.Companion.CAMPAIGN_NOT_EXISTENT +import com.asfoundation.wallet.entity.SubmitPoAException.Companion.INCORRECT_DATA +import com.asfoundation.wallet.entity.SubmitPoAException.Companion.NOT_AVAILABLE_FOR_COUNTRY +import com.asfoundation.wallet.entity.SubmitPoAException.Companion.NOT_ENOUGH_BUDGET +import com.asfoundation.wallet.poa.BackEndErrorMapper.BackEndError.* +import retrofit2.HttpException +import java.net.UnknownHostException + +class BackEndErrorMapper { + + fun map(throwable: Throwable): BackEndError { + if (throwable is UnknownHostException) { + return NO_INTERNET + } + if (throwable is SubmitPoAException) { + when (throwable.error) { + CAMPAIGN_NOT_EXISTENT, + CAMPAIGN_NOT_AVAILABLE, + NOT_ENOUGH_BUDGET -> return BACKEND_CAMPAIGN_NOT_AVAILABLE + NOT_AVAILABLE_FOR_COUNTRY -> return BACKEND_CAMPAIGN_NOT_AVAILABLE_ON_COUNTRY + ALREADY_SUBMITTED_FOR_IP, + ALREADY_SUBMITTED_FOR_WALLET -> return BACKEND_ALREADY_AWARDED + INCORRECT_DATA -> return BACKEND_INVALID_DATA + } + } + if (throwable is HttpException) { + when (throwable.code()) { + 401 -> return BACKEND_PHONE_NOT_VERIFIED + } + } + return BACKEND_GENERIC_ERROR + } + + enum class BackEndError { + BACKEND_GENERIC_ERROR, + BACKEND_CAMPAIGN_NOT_AVAILABLE, + BACKEND_CAMPAIGN_NOT_AVAILABLE_ON_COUNTRY, + BACKEND_ALREADY_AWARDED, + BACKEND_INVALID_DATA, + BACKEND_PHONE_NOT_VERIFIED, + NO_INTERNET + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/poa/BlockchainErrorMapper.java b/app/src/main/java/com/asfoundation/wallet/poa/BlockchainErrorMapper.java index 2ba357c8b6c..7c76fe768ac 100644 --- a/app/src/main/java/com/asfoundation/wallet/poa/BlockchainErrorMapper.java +++ b/app/src/main/java/com/asfoundation/wallet/poa/BlockchainErrorMapper.java @@ -13,9 +13,9 @@ public class BlockchainErrorMapper { - public static final String INSUFFICIENT_ERROR_MESSAGE = + private static final String INSUFFICIENT_ERROR_MESSAGE = "insufficient funds for gas * price + value"; - public static final String NONCE_TOO_LOW_ERROR_MESSAGE = "nonce too low"; + private static final String NONCE_TOO_LOW_ERROR_MESSAGE = "nonce too low"; public BlockchainError map(Throwable throwable) { if (throwable instanceof UnknownHostException) { diff --git a/app/src/main/java/com/asfoundation/wallet/poa/CountryCodeProvider.java b/app/src/main/java/com/asfoundation/wallet/poa/CountryCodeProvider.java new file mode 100644 index 00000000000..c19e6870311 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/poa/CountryCodeProvider.java @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.poa; + +import io.reactivex.Single; + +public interface CountryCodeProvider { + Single getCountryCode(); +} diff --git a/app/src/main/java/com/asfoundation/wallet/poa/DataMapper.java b/app/src/main/java/com/asfoundation/wallet/poa/DataMapper.java index 041c0e70917..cfc5bdaff9e 100644 --- a/app/src/main/java/com/asfoundation/wallet/poa/DataMapper.java +++ b/app/src/main/java/com/asfoundation/wallet/poa/DataMapper.java @@ -13,6 +13,7 @@ import org.web3j.abi.datatypes.Function; import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.Utf8String; +import org.web3j.abi.datatypes.generated.Bytes2; import org.web3j.abi.datatypes.generated.Bytes32; import org.web3j.abi.datatypes.generated.Uint64; import org.web3j.utils.Numeric; @@ -34,9 +35,10 @@ public byte[] getData(Proof proof) { Address storeAddress = new Address(proof.getStoreAddress()); Address oemAddress = new Address(proof.getOemAddress()); Utf8String walletName = new Utf8String(proof.getWalletPackage()); + Bytes2 countryCode = new Bytes2(convertCountryCode(proof.getCountryCode())); List params = Arrays.asList(packageName, bidId, new DynamicArray<>(timeStampList), - new DynamicArray<>(nonceList), storeAddress, oemAddress, walletName); + new DynamicArray<>(nonceList), storeAddress, oemAddress, walletName, countryCode); List> returnTypes = Collections.singletonList(new TypeReference() { }); Function function = new Function("registerPoA", params, returnTypes); @@ -51,4 +53,15 @@ private Bytes32 stringToBytes32(String string) { bidId.toByteArray().length); return new Bytes32(value); } + + public byte[] convertCountryCode(String countryCode) { + byte[] data = new byte[2]; + char[] chars = countryCode.toCharArray(); + //mapDarkIcons country code for contract's format + int index = ((int) chars[0] - 65) * 26 + ((int) chars[1] - 65); + + data[0] = (byte) ((index >>> 8) & 0xFF); + data[1] = (byte) (index & 0xFF); + return data; + } } diff --git a/app/src/main/java/com/asfoundation/wallet/poa/HashCalculator.java b/app/src/main/java/com/asfoundation/wallet/poa/HashCalculator.java index 23f26564cc8..746a40c45b8 100644 --- a/app/src/main/java/com/asfoundation/wallet/poa/HashCalculator.java +++ b/app/src/main/java/com/asfoundation/wallet/poa/HashCalculator.java @@ -1,6 +1,5 @@ package com.asfoundation.wallet.poa; -import java.nio.ByteBuffer; import java.security.NoSuchAlgorithmException; public class HashCalculator { @@ -13,29 +12,15 @@ public HashCalculator(int nonceLeadingZeros, Calculator calculator) { } public long calculateNonce(NonceData nonceData) throws NoSuchAlgorithmException { - int bytesSize; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { - bytesSize = Long.BYTES; - } else { - bytesSize = 8; - } + String data = nonceData.getPackageName() + nonceData.getTimeStamp(); + String hash = calculator.calculate(data.getBytes()); - ByteBuffer buffer = ByteBuffer.allocate(nonceData.getPackageName() - .length() + bytesSize); - buffer.put(nonceData.getPackageName() - .getBytes()); - buffer.putLong(nonceData.getTimeStamp()); - String hash = calculator.calculate(buffer.array()); - - buffer = ByteBuffer.allocate(bytesSize + hash.length()); String result; long nonce = -1; do { - buffer.clear(); nonce++; - buffer.putLong(nonce); - buffer.put(hash.getBytes()); - result = calculator.calculate(buffer.array()); + data = nonce + hash; + result = calculator.calculate(data.getBytes()); } while (!result.substring(0, leadingString.length()) .equals(leadingString)); return nonce; diff --git a/app/src/main/java/com/asfoundation/wallet/poa/PoaInformationModel.kt b/app/src/main/java/com/asfoundation/wallet/poa/PoaInformationModel.kt new file mode 100644 index 00000000000..a79154c717f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/poa/PoaInformationModel.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.poa + +data class PoaInformationModel(val remainingPoa: Int, val remainingHours: Int, + val remainingMinutes: Int) { + + fun hasRemainingPoa() = remainingPoa != 0 && (remainingHours >= 0 || remainingMinutes >= 0) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/poa/Proof.java b/app/src/main/java/com/asfoundation/wallet/poa/Proof.java index bbcf764dbb9..f5d72d30494 100644 --- a/app/src/main/java/com/asfoundation/wallet/poa/Proof.java +++ b/app/src/main/java/com/asfoundation/wallet/poa/Proof.java @@ -1,8 +1,8 @@ package com.asfoundation.wallet.poa; -import java.math.BigDecimal; import java.util.Collections; import java.util.List; +import java.util.Objects; import javax.annotation.Nullable; public class Proof { @@ -11,8 +11,7 @@ public class Proof { private final List proofComponentList; private final ProofStatus proofStatus; private final int chainId; - private final BigDecimal gasPrice; - private final BigDecimal gasLimit; + @Nullable private final String countryCode; @Nullable private final String campaignId; @Nullable private final String oemAddress; @Nullable private final String storeAddress; @@ -20,8 +19,8 @@ public class Proof { public Proof(String packageName, @Nullable String campaignId, List proofComponentList, String walletPackage, ProofStatus proofStatus, - int chainId, @Nullable String oemAddress, @Nullable String storeAddress, BigDecimal gasPrice, - BigDecimal gasLimit, @Nullable String hash) { + int chainId, @Nullable String oemAddress, @Nullable String storeAddress, + @Nullable String hash, @Nullable String countryCode) { this.packageName = packageName; this.campaignId = campaignId; this.proofComponentList = proofComponentList; @@ -30,34 +29,28 @@ public Proof(String packageName, @Nullable String campaignId, this.chainId = chainId; this.oemAddress = oemAddress; this.storeAddress = storeAddress; - this.gasPrice = gasPrice; - this.gasLimit = gasLimit; this.hash = hash; + this.countryCode = countryCode; } public Proof(String packageName, @Nullable String campaignId, List proofComponentList, String walletPackage, ProofStatus proofStatus, - int chainId, @Nullable String oemAddress, @Nullable String storeAddress, BigDecimal gasPrice, - BigDecimal gasLimit) { + int chainId, @Nullable String oemAddress, @Nullable String storeAddress, String countryCode) { this(packageName, campaignId, proofComponentList, walletPackage, proofStatus, chainId, - oemAddress, storeAddress, gasPrice, gasLimit, null); + oemAddress, storeAddress, null, countryCode); } public Proof(String packageName, String walletPackage, ProofStatus proofStatus, int chainId) { this(packageName, null, Collections.emptyList(), walletPackage, proofStatus, chainId, null, - null, BigDecimal.ZERO, BigDecimal.ZERO, null); + null, null, null); } - @Nullable public String getHash() { - return hash; - } - - public BigDecimal getGasPrice() { - return gasPrice; + @Nullable public String getCountryCode() { + return countryCode; } - public BigDecimal getGasLimit() { - return gasLimit; + @Nullable public String getHash() { + return hash; } public String getOemAddress() { @@ -98,11 +91,11 @@ public String getPackageName() { result = 31 * result + proofComponentList.hashCode(); result = 31 * result + proofStatus.hashCode(); result = 31 * result + chainId; - result = 31 * result + gasPrice.hashCode(); - result = 31 * result + gasLimit.hashCode(); + result = 31 * result + (countryCode != null ? countryCode.hashCode() : 0); result = 31 * result + (campaignId != null ? campaignId.hashCode() : 0); result = 31 * result + (oemAddress != null ? oemAddress.hashCode() : 0); result = 31 * result + (storeAddress != null ? storeAddress.hashCode() : 0); + result = 31 * result + (hash != null ? hash.hashCode() : 0); return result; } @@ -117,16 +110,19 @@ public String getPackageName() { if (!walletPackage.equals(proof.walletPackage)) return false; if (!proofComponentList.equals(proof.proofComponentList)) return false; if (proofStatus != proof.proofStatus) return false; - if (!gasPrice.equals(proof.gasPrice)) return false; - if (!gasLimit.equals(proof.gasLimit)) return false; - if (campaignId != null ? !campaignId.equals(proof.campaignId) : proof.campaignId != null) { + if (!Objects.equals(countryCode, proof.countryCode)) { return false; } - if (oemAddress != null ? !oemAddress.equals(proof.oemAddress) : proof.oemAddress != null) { + if (!Objects.equals(campaignId, proof.campaignId)) { return false; } - return storeAddress != null ? storeAddress.equals(proof.storeAddress) - : proof.storeAddress == null; + if (!Objects.equals(oemAddress, proof.oemAddress)) { + return false; + } + if (!Objects.equals(storeAddress, proof.storeAddress)) { + return false; + } + return Objects.equals(hash, proof.hash); } @Override public String toString() { @@ -143,10 +139,9 @@ public String getPackageName() { + proofStatus + ", chainId=" + chainId - + ", gasPrice=" - + gasPrice - + ", gasLimit=" - + gasLimit + + ", countryCode='" + + countryCode + + '\'' + ", campaignId='" + campaignId + '\'' @@ -156,6 +151,9 @@ public String getPackageName() { + ", storeAddress='" + storeAddress + '\'' + + ", hash='" + + hash + + '\'' + '}'; } } diff --git a/app/src/main/java/com/asfoundation/wallet/poa/ProofOfAttentionService.java b/app/src/main/java/com/asfoundation/wallet/poa/ProofOfAttentionService.java index 0290af003d6..cfdf9f5842b 100644 --- a/app/src/main/java/com/asfoundation/wallet/poa/ProofOfAttentionService.java +++ b/app/src/main/java/com/asfoundation/wallet/poa/ProofOfAttentionService.java @@ -1,14 +1,21 @@ package com.asfoundation.wallet.poa; -import android.support.annotation.NonNull; -import com.asfoundation.wallet.repository.Repository; +import androidx.annotation.NonNull; +import com.appcoins.wallet.bdsbilling.WalletService; +import com.appcoins.wallet.commons.Repository; +import com.asfoundation.wallet.advertise.Advertising; +import com.asfoundation.wallet.advertise.CampaignInteract; +import com.asfoundation.wallet.billing.partners.AddressService; import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Scheduler; import io.reactivex.Single; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.subjects.BehaviorSubject; +import io.reactivex.subjects.Subject; import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang3.StringUtils; public class ProofOfAttentionService { private final Repository cache; @@ -18,13 +25,20 @@ public class ProofOfAttentionService { private final ProofWriter proofWriter; private final int maxNumberProofComponents; private final Scheduler computationScheduler; - private final BlockchainErrorMapper errorMapper; + private final BackEndErrorMapper errorMapper; private final TaggedCompositeDisposable disposables; + private final CountryCodeProvider countryCodeProvider; + private final AddressService partnerAddressService; + private final Advertising campaignInteract; + private final WalletService walletService; + private final Subject walletValidated; public ProofOfAttentionService(Repository cache, String walletPackage, HashCalculator hashCalculator, CompositeDisposable compositeDisposable, ProofWriter proofWriter, Scheduler computationScheduler, int maxNumberProofComponents, - BlockchainErrorMapper errorMapper, TaggedCompositeDisposable disposables) { + BackEndErrorMapper errorMapper, TaggedCompositeDisposable disposables, + CountryCodeProvider countryCodeProvider, AddressService partnerAddressService, + WalletService walletService, CampaignInteract campaignInteract) { this.cache = cache; this.walletPackage = walletPackage; this.hashCalculator = hashCalculator; @@ -34,55 +48,92 @@ public ProofOfAttentionService(Repository cache, String walletPac this.maxNumberProofComponents = maxNumberProofComponents; this.errorMapper = errorMapper; this.disposables = disposables; + this.countryCodeProvider = countryCodeProvider; + this.partnerAddressService = partnerAddressService; + this.campaignInteract = campaignInteract; + this.walletValidated = BehaviorSubject.create(); + this.walletService = walletService; } public void start() { compositeDisposable.add(getReadyPoA().observeOn(computationScheduler) - .flatMapSingle(proof -> writeOnBlockChain(proof).doOnError( + .flatMapSingle(proof -> submitProof(proof).doOnError( throwable -> handleError(throwable, proof.getPackageName())) .doOnSubscribe( disposable -> updateProofStatus(proof.getPackageName(), ProofStatus.SUBMITTING))) .retry() .subscribe()); + + compositeDisposable.add(getTerminatedValidationProcess().observeOn(computationScheduler) + .flatMap(isPoaReady -> getReadyPoAResume().flatMapSingle( + proof -> submitProof(proof).doOnError( + throwable -> handleError(throwable, proof.getPackageName())) + .doOnSubscribe(disposable -> updateProofStatus(proof.getPackageName(), + ProofStatus.SUBMITTING)))) + .retry() + .subscribe()); + + compositeDisposable.add(getReadyCountryCode().observeOn(computationScheduler) + .flatMapSingle(proof -> countryCodeProvider.getCountryCode() + .doOnSuccess(countryCode -> setCountryCodeSync(proof.getPackageName(), countryCode)) + .doOnError(throwable -> handleError(throwable, proof.getPackageName()))) + .retry() + .subscribe()); + } + + private void setCountryCodeSync(String packageName, String countryCode) { + synchronized (this) { + Proof proof = getPreviousProofSync(packageName); + cache.saveSync(packageName, + new Proof(packageName, proof.getCampaignId(), proof.getProofComponentList(), + walletPackage, ProofStatus.PROCESSING, proof.getChainId(), proof.getOemAddress(), + proof.getStoreAddress(), proof.getHash(), countryCode)); + } } private void handleError(Throwable throwable, String proofPackageName) { ProofStatus proofStatus; switch (errorMapper.map(throwable)) { - default: - case WRONG_NETWORK: - case UNKNOWN_TOKEN: - case NONCE_ERROR: - case INVALID_BLOCKCHAIN_ERROR: - case TRANSACTION_NOT_FOUND: - throwable.printStackTrace(); - proofStatus = ProofStatus.GENERAL_ERROR; + case BACKEND_CAMPAIGN_NOT_AVAILABLE: + proofStatus = ProofStatus.NOT_AVAILABLE; + break; + case BACKEND_CAMPAIGN_NOT_AVAILABLE_ON_COUNTRY: + proofStatus = ProofStatus.NOT_AVAILABLE_ON_COUNTRY; break; - case NO_FUNDS: - proofStatus = ProofStatus.NO_FUNDS; + case BACKEND_ALREADY_AWARDED: + proofStatus = ProofStatus.ALREADY_REWARDED; + break; + case BACKEND_INVALID_DATA: + proofStatus = ProofStatus.INVALID_DATA; break; case NO_INTERNET: proofStatus = ProofStatus.NO_INTERNET; break; - case NO_WALLET: - proofStatus = ProofStatus.NO_WALLET; + case BACKEND_PHONE_NOT_VERIFIED: + proofStatus = ProofStatus.PHONE_NOT_VERIFIED; + break; + case BACKEND_GENERIC_ERROR: + default: + throwable.printStackTrace(); + proofStatus = ProofStatus.GENERAL_ERROR; break; } + updateProofStatus(proofPackageName, proofStatus); } - private Single writeOnBlockChain(Proof proof) { + private Single submitProof(Proof proof) { Proof completedProof = new Proof(proof.getPackageName(), proof.getCampaignId(), proof.getProofComponentList(), proof.getWalletPackage(), ProofStatus.SUBMITTING, proof.getChainId(), - proof.getOemAddress(), proof.getStoreAddress(), proof.getGasPrice(), - proof.getGasLimit()); + proof.getOemAddress(), proof.getStoreAddress(), proof.getHash(), + proof.getCountryCode()); return proofWriter.writeProof(completedProof) .doOnSuccess(hash -> cache.saveSync(completedProof.getPackageName(), new Proof(completedProof.getPackageName(), completedProof.getCampaignId(), completedProof.getProofComponentList(), walletPackage, ProofStatus.COMPLETED, - proof.getChainId(), proof.getOemAddress(), proof.getStoreAddress(), - proof.getGasPrice(), proof.getGasLimit(), hash))); + proof.getChainId(), proof.getOemAddress(), proof.getStoreAddress(), hash, + proof.getCountryCode()))); } public void stop() { @@ -100,26 +151,29 @@ public void setCampaignId(String packageName, String campaignId) { private void setCampaignIdSync(String packageName, String campaignId) { synchronized (this) { Proof proof = getPreviousProofSync(packageName); - cache.saveSync(packageName, - new Proof(packageName, campaignId, proof.getProofComponentList(), walletPackage, - ProofStatus.PROCESSING, proof.getChainId(), proof.getOemAddress(), - proof.getStoreAddress(), proof.getGasPrice(), proof.getGasLimit())); + if (areComponentsMissing(proof) || StringUtils.equals(proof.getCampaignId(), campaignId)) { + cache.saveSync(packageName, + new Proof(packageName, campaignId, proof.getProofComponentList(), walletPackage, + ProofStatus.PROCESSING, proof.getChainId(), proof.getOemAddress(), + proof.getStoreAddress(), proof.getHash(), proof.getCountryCode())); + } } } public void setChainId(String packageName, int chainId) { disposables.add(packageName, Completable.fromAction(() -> setChainIdSync(packageName, chainId)) - .subscribeOn(computationScheduler) .subscribe()); } private void setChainIdSync(String packageName, int chainId) { synchronized (this) { Proof proof = getPreviousProofSync(packageName); - cache.saveSync(packageName, - new Proof(packageName, proof.getCampaignId(), proof.getProofComponentList(), - walletPackage, ProofStatus.PROCESSING, chainId, proof.getOemAddress(), - proof.getStoreAddress(), proof.getGasPrice(), proof.getGasLimit())); + if (areComponentsMissing(proof) && proof.getChainId() != chainId) { + cache.saveSync(packageName, + new Proof(packageName, proof.getCampaignId(), proof.getProofComponentList(), + walletPackage, ProofStatus.PROCESSING, chainId, proof.getOemAddress(), + proof.getStoreAddress(), proof.getHash(), proof.getCountryCode())); + } } } @@ -129,7 +183,7 @@ private void updateProofStatus(String packageName, ProofStatus proofStatus) { cache.saveSync(packageName, new Proof(packageName, proof.getCampaignId(), proof.getProofComponentList(), walletPackage, proofStatus, proof.getChainId(), proof.getOemAddress(), - proof.getStoreAddress(), proof.getGasPrice(), proof.getGasLimit())); + proof.getStoreAddress(), proof.getHash(), proof.getCountryCode())); } } @@ -138,18 +192,21 @@ private void setSetProofSync(String packageName, long timeStamp, long nonce) { Proof proof = getPreviousProofSync(packageName); cache.saveSync(packageName, new Proof(proof.getPackageName(), proof.getCampaignId(), createProofComponentList(timeStamp, nonce, proof), walletPackage, ProofStatus.PROCESSING, - proof.getChainId(), proof.getOemAddress(), proof.getStoreAddress(), proof.getGasPrice(), - proof.getGasLimit())); + proof.getChainId(), proof.getOemAddress(), proof.getStoreAddress(), proof.getHash(), + proof.getCountryCode())); } } public void registerProof(String packageName, long timeStamp) { - disposables.add(packageName, Single.defer( - () -> Single.just(hashCalculator.calculateNonce(new NonceData(timeStamp, packageName)))) - .doOnSuccess(nonce -> setSetProofSync(packageName, timeStamp, nonce)) - .toCompletable() - .subscribeOn(computationScheduler) - .subscribe()); + Proof proof = getPreviousProofSync(packageName); + if (areComponentsMissing(proof)) { + disposables.add(packageName, Observable.fromCallable( + () -> hashCalculator.calculateNonce(new NonceData(timeStamp, packageName))) + .doOnNext(nonce -> setSetProofSync(packageName, timeStamp, nonce)) + .ignoreElements() + .subscribeOn(computationScheduler) + .subscribe()); + } } @NonNull @@ -182,13 +239,63 @@ private Observable getReadyPoA() { .filter(this::isReadyToComputePoAId)); } + private Observable getReadyPoAResume() { + return cache.getAll() + .flatMap(proofs -> Observable.fromIterable(proofs) + .filter(this::isReadyToResumePoAId)); + } + + private Observable getTerminatedValidationProcess() { + return walletValidated.map(validated -> validated) + .filter(validated -> validated); + } + + public void setWalletValidated() { + walletValidated.onNext(true); + } + private boolean isReadyToComputePoAId(Proof proof) { return proof.getCampaignId() != null && !proof.getCampaignId() .isEmpty() && proof.getProofComponentList() - .size() == maxNumberProofComponents && proof.getProofStatus() - .equals(ProofStatus.PROCESSING); + .size() == maxNumberProofComponents + && proof.getProofStatus() + .equals(ProofStatus.PROCESSING) + && proof.getCountryCode() != null; + } + + private boolean isReadyToResumePoAId(Proof proof) { + return proof.getCampaignId() != null + && !proof.getCampaignId() + .isEmpty() + && proof.getProofComponentList() + .size() == maxNumberProofComponents + && proof.getProofStatus() + .equals(ProofStatus.PHONE_NOT_VERIFIED) + && proof.getCountryCode() != null; + } + + private Observable getReadyCountryCode() { + return cache.getAll() + .flatMap(proofs -> Observable.fromIterable(proofs) + .filter(this::isReadyToGetCountryCode)); + } + + private boolean isReadyToGetCountryCode(Proof proof) { + return proof.getCampaignId() != null + && !proof.getCampaignId() + .isEmpty() + && proof.getProofComponentList() + .size() == maxNumberProofComponents + && proof.getProofStatus() + .equals(ProofStatus.PROCESSING) + && proof.getCountryCode() == null; + } + + private boolean areComponentsMissing(Proof proof) { + return proof.getProofComponentList() + .size() < maxNumberProofComponents; } public Observable> get() { @@ -206,61 +313,61 @@ public void cancel(String packageName) { updateProofStatus(packageName, ProofStatus.CANCELLED); } - public void setOemAddress(String packageName, String address) { - disposables.add(packageName, - Completable.fromAction(() -> setOemAddressSync(packageName, address)) - .subscribeOn(computationScheduler) - .subscribe()); + public void setOemAddress(String packageName) { + disposables.add(packageName, partnerAddressService.getOemAddressForPackage(packageName) + .flatMapCompletable( + address -> Completable.fromAction(() -> setOemAddressSync(packageName, address))) + .subscribeOn(computationScheduler) + .subscribe()); } private void setOemAddressSync(String packageName, String address) { synchronized (this) { Proof proof = getPreviousProofSync(packageName); - cache.saveSync(packageName, - new Proof(packageName, proof.getCampaignId(), proof.getProofComponentList(), - walletPackage, ProofStatus.PROCESSING, proof.getChainId(), address, - proof.getStoreAddress(), proof.getGasPrice(), proof.getGasLimit())); - } - } - - private void setGasSettingsSync(String packageName, - ProofSubmissionFeeData proofSubmissionFeeData) { - synchronized (this) { - Proof proof = getPreviousProofSync(packageName); - cache.saveSync(packageName, - new Proof(packageName, proof.getCampaignId(), proof.getProofComponentList(), - walletPackage, ProofStatus.PROCESSING, proof.getChainId(), proof.getOemAddress(), - proof.getStoreAddress(), proofSubmissionFeeData.getGasPrice(), - proofSubmissionFeeData.getGasLimit())); + if (areComponentsMissing(proof) || StringUtils.equals(proof.getOemAddress(), address)) { + cache.saveSync(packageName, + new Proof(packageName, proof.getCampaignId(), proof.getProofComponentList(), + walletPackage, ProofStatus.PROCESSING, proof.getChainId(), address, + proof.getStoreAddress(), proof.getHash(), proof.getCountryCode())); + } } } - public void setStoreAddress(String packageName, String address) { - disposables.add(packageName, - Completable.fromAction(() -> setStoreAddressSync(packageName, address)) - .subscribeOn(computationScheduler) - .subscribe()); + public void setStoreAddress(String packageName) { + disposables.add(packageName, partnerAddressService.getStoreAddressForPackage(packageName) + .flatMapCompletable( + address -> Completable.fromAction(() -> setStoreAddressSync(packageName, address))) + .subscribeOn(computationScheduler) + .subscribe()); } private void setStoreAddressSync(String packageName, String address) { synchronized (this) { Proof proof = getPreviousProofSync(packageName); - cache.saveSync(packageName, - new Proof(packageName, proof.getCampaignId(), proof.getProofComponentList(), - walletPackage, ProofStatus.PROCESSING, proof.getChainId(), proof.getOemAddress(), - address, proof.getGasPrice(), proof.getGasLimit())); + if (areComponentsMissing(proof) || StringUtils.equals(proof.getStoreAddress(), address)) { + cache.saveSync(packageName, + new Proof(packageName, proof.getCampaignId(), proof.getProofComponentList(), + walletPackage, ProofStatus.PROCESSING, proof.getChainId(), proof.getOemAddress(), + address, proof.getHash(), proof.getCountryCode())); + } } } - public Single isWalletReady(String packageName) { + public Single isWalletReady(int chainId, String packageName, + int versionCode) { return Single.defer(() -> { synchronized (this) { - return proofWriter.hasEnoughFunds(getPreviousProofSync(packageName).getChainId()); + return campaignInteract.hasWalletPrepared(chainId, packageName, versionCode); } - }) - .doOnSuccess( - proofSubmissionFeeData -> setGasSettingsSync(packageName, proofSubmissionFeeData)) - .subscribeOn(computationScheduler) - .map(ProofSubmissionFeeData::getStatus); + }); + } + + public Single handleCreateWallet() { + return walletService.getWalletOrCreate(); + } + + public Single retrievePoaInformation() { + return walletService.getWalletAddress() + .flatMap(campaignInteract::retrievePoaInformation); } } diff --git a/app/src/main/java/com/asfoundation/wallet/poa/ProofStatus.java b/app/src/main/java/com/asfoundation/wallet/poa/ProofStatus.java deleted file mode 100644 index 827cc14ede6..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/poa/ProofStatus.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.asfoundation.wallet.poa; - -public enum ProofStatus { - PROCESSING, SUBMITTING, COMPLETED, NO_FUNDS, NO_INTERNET, GENERAL_ERROR, NO_WALLET, CANCELLED; - - public boolean isTerminate() { - switch (this) { - case PROCESSING: - case SUBMITTING: - return false; - case COMPLETED: - case NO_FUNDS: - case NO_INTERNET: - case GENERAL_ERROR: - case NO_WALLET: - case CANCELLED: - default: - return true; - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/poa/ProofStatus.kt b/app/src/main/java/com/asfoundation/wallet/poa/ProofStatus.kt new file mode 100644 index 00000000000..5e51f6bd61d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/poa/ProofStatus.kt @@ -0,0 +1,25 @@ +package com.asfoundation.wallet.poa + +enum class ProofStatus { + PROCESSING, SUBMITTING, COMPLETED, NO_FUNDS, NO_INTERNET, GENERAL_ERROR, NO_WALLET, CANCELLED, + NOT_AVAILABLE, NOT_AVAILABLE_ON_COUNTRY, ALREADY_REWARDED, INVALID_DATA, PHONE_NOT_VERIFIED; + + val isTerminate: Boolean + get() { + return when (this) { + PROCESSING, + SUBMITTING, + PHONE_NOT_VERIFIED -> false + COMPLETED, + NO_FUNDS, + NO_INTERNET, + GENERAL_ERROR, + NO_WALLET, + CANCELLED, + NOT_AVAILABLE, + NOT_AVAILABLE_ON_COUNTRY, + ALREADY_REWARDED, + INVALID_DATA -> true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/poa/ProofSubmissionData.kt b/app/src/main/java/com/asfoundation/wallet/poa/ProofSubmissionData.kt new file mode 100644 index 00000000000..550e0beb319 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/poa/ProofSubmissionData.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.poa + +data class ProofSubmissionData(val status: RequirementsStatus, val hoursRemaining: Int = 0, + val minutesRemaining: Int = 0) { + constructor(status: RequirementsStatus) : this(status, 0, 0) + + fun hasReachedPoaLimit() = hoursRemaining != 0 || minutesRemaining != 0 + + enum class RequirementsStatus { + READY, NO_FUNDS, NO_NETWORK, NO_WALLET, NOT_ELIGIBLE, WRONG_NETWORK, UNKNOWN_NETWORK, + UPDATE_REQUIRED + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/poa/ProofSubmissionFeeData.java b/app/src/main/java/com/asfoundation/wallet/poa/ProofSubmissionFeeData.java deleted file mode 100644 index 4e44cacd896..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/poa/ProofSubmissionFeeData.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.asfoundation.wallet.poa; - -import java.math.BigDecimal; - -public class ProofSubmissionFeeData { - private final BigDecimal gasLimit; - private final BigDecimal gasPrice; - private final RequirementsStatus status; - - public ProofSubmissionFeeData(RequirementsStatus status, BigDecimal gasLimit, - BigDecimal gasPrice) { - this.gasLimit = gasLimit; - this.gasPrice = gasPrice; - this.status = status; - } - - public BigDecimal getGasLimit() { - return gasLimit; - } - - public BigDecimal getGasPrice() { - return gasPrice; - } - - public RequirementsStatus getStatus() { - return status; - } - - public enum RequirementsStatus { - READY, NO_FUNDS, NO_NETWORK, NO_WALLET - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/poa/ProofWriter.java b/app/src/main/java/com/asfoundation/wallet/poa/ProofWriter.java index 07b2bb31e9d..68c9acaa940 100644 --- a/app/src/main/java/com/asfoundation/wallet/poa/ProofWriter.java +++ b/app/src/main/java/com/asfoundation/wallet/poa/ProofWriter.java @@ -4,6 +4,5 @@ public interface ProofWriter { Single writeProof(Proof proof); - - Single hasEnoughFunds(int chainId); } + diff --git a/app/src/main/java/com/asfoundation/wallet/poa/StatelessProof.kt b/app/src/main/java/com/asfoundation/wallet/poa/StatelessProof.kt deleted file mode 100644 index ce4b38a1d7b..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/poa/StatelessProof.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.asfoundation.wallet.poa - -data class StatelessProof(val packageName: String, val campaignId: String, - val proofComponentList: List, - val proofId: String?, val walletPackage: String) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/poa/TransactionFactory.java b/app/src/main/java/com/asfoundation/wallet/poa/TransactionFactory.java deleted file mode 100644 index 3dee172c72c..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/poa/TransactionFactory.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.asfoundation.wallet.poa; - -import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.interact.DefaultTokenProvider; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; -import com.asfoundation.wallet.repository.PasswordStore; -import com.asfoundation.wallet.repository.WalletRepositoryType; -import com.asfoundation.wallet.repository.Web3jProvider; -import com.asfoundation.wallet.service.AccountKeystoreService; -import io.reactivex.Single; -import java.io.IOException; -import java.math.BigDecimal; -import org.web3j.protocol.core.DefaultBlockParameterName; - -public class TransactionFactory { - private final Web3jProvider web3jProvider; - private final WalletRepositoryType walletRepositoryType; - private final AccountKeystoreService accountKeystoreService; - private final PasswordStore passwordStore; - private final DefaultTokenProvider defaultTokenProvider; - private final EthereumNetworkRepositoryType networkRepositoryType; - private final DataMapper dataMapper; - - public TransactionFactory(Web3jProvider web3jProvider, WalletRepositoryType walletRepositoryType, - AccountKeystoreService accountKeystoreService, PasswordStore passwordStore, - DefaultTokenProvider defaultTokenProvider, - EthereumNetworkRepositoryType networkRepositoryType, DataMapper dataMapper) { - this.web3jProvider = web3jProvider; - this.walletRepositoryType = walletRepositoryType; - this.accountKeystoreService = accountKeystoreService; - this.passwordStore = passwordStore; - this.defaultTokenProvider = defaultTokenProvider; - this.networkRepositoryType = networkRepositoryType; - this.dataMapper = dataMapper; - } - - public Single createTransaction(Proof proof) { - return Single.just(networkRepositoryType.getDefaultNetwork()) - .flatMap(defaultNetworkInfo -> defaultTokenProvider.getAdsAddress(proof.getChainId()) - .doOnSubscribe(disposable -> setNetwork(proof.getChainId())) - .flatMap(adsAddress -> walletRepositoryType.getDefaultWallet() - .flatMap(wallet -> passwordStore.getPassword(wallet) - .flatMap( - password -> accountKeystoreService.signTransaction(wallet.address, password, - adsAddress, BigDecimal.ZERO, proof.getGasPrice(), proof.getGasLimit(), - getNonce(wallet.address), dataMapper.getData(proof), - proof.getChainId())))) - .doAfterTerminate( - () -> networkRepositoryType.setDefaultNetworkInfo(defaultNetworkInfo))); - } - - public void setNetwork(int chainId) { - for (NetworkInfo networkInfo : networkRepositoryType.getAvailableNetworkList()) { - if (chainId == networkInfo.chainId) { - networkRepositoryType.setDefaultNetworkInfo(networkInfo); - } - } - } - - private long getNonce(String address) throws IOException { - return web3jProvider.getDefault() - .ethGetTransactionCount(address, DefaultBlockParameterName.LATEST) - .send() - .getTransactionCount() - .longValue(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/Promotion.kt b/app/src/main/java/com/asfoundation/wallet/promotions/Promotion.kt new file mode 100644 index 00000000000..3a5858a99ff --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/Promotion.kt @@ -0,0 +1,73 @@ +package com.asfoundation.wallet.promotions + +import android.graphics.drawable.Drawable +import androidx.annotation.StringRes +import java.math.BigDecimal + +open class Promotion(val id: String) + +open class PerkPromotion(id: String, val startDate: Long?, val endDate: Long, + val detailsLink: String?) : Promotion(id) + +class TitleItem( + @StringRes val title: Int, + @StringRes val subtitle: Int, + val isGamificationTitle: Boolean, + val bonus: String = "0.0", + id: String = "" +) : Promotion(id) + +class DefaultItem( + id: String, + val description: String, + val icon: String?, + startDate: Long?, + endDate: Long, + detailsLink: String? +) : PerkPromotion(id, startDate, endDate, detailsLink) + +class FutureItem( + id: String, + val description: String, + val icon: String?, + startDate: Long?, + endDate: Long, + detailsLink: String? +) : PerkPromotion(id, startDate, endDate, detailsLink) + +class ProgressItem( + id: String, + val description: String, + val icon: String?, + startDate: Long?, + endDate: Long, + val current: BigDecimal, + val objective: BigDecimal?, + detailsLink: String? +) : PerkPromotion(id, startDate, endDate, detailsLink) + +class GamificationItem( + id: String, + val planet: Drawable?, + val level: Int, + val levelColor: Int, + val title: String, + val toNextLevelAmount: BigDecimal?, + var bonus: Double, + val links: MutableList +) : Promotion(id) + +class ReferralItem( + id: String, + val bonus: BigDecimal, + val currency: String, + val link: String +) : Promotion(id) + +class GamificationLinkItem( + id: String, + val description: String, + val icon: String?, + startDate: Long?, + endDate: Long +) : PerkPromotion(id, startDate, endDate, null) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionClick.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionClick.kt new file mode 100644 index 00000000000..6141f552fe9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionClick.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.promotions + +data class PromotionClick( + val id: String, + val extras: Map? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionNotification.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionNotification.kt new file mode 100644 index 00000000000..388a1fe6c50 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionNotification.kt @@ -0,0 +1,12 @@ +package com.asfoundation.wallet.promotions + +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction + +data class PromotionNotification(override val positiveAction: CardNotificationAction, + val noResTitle: String?, + val noResBody: String?, + val noResIcon: String?, + val id: String, + val detailsLink: String?) : + CardNotification(null, null, null, null, positiveAction) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionUpdateScreen.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionUpdateScreen.kt new file mode 100644 index 00000000000..bda8e3197a9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionUpdateScreen.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.promotions + +enum class PromotionUpdateScreen { + PROMOTIONS, + TRANSACTIONS +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsActivity.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsActivity.kt new file mode 100644 index 00000000000..c8ad9864232 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsActivity.kt @@ -0,0 +1,89 @@ +package com.asfoundation.wallet.promotions + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.MenuItem +import android.widget.Toast +import androidx.core.app.ShareCompat +import com.asf.wallet.R +import com.asfoundation.wallet.referrals.InviteFriendsActivity +import com.asfoundation.wallet.router.TransactionsRouter +import com.asfoundation.wallet.ui.BaseActivity +import com.asfoundation.wallet.ui.gamification.GamificationActivity +import io.reactivex.subjects.PublishSubject + +class PromotionsActivity : BaseActivity(), PromotionsActivityView { + + private lateinit var transactionsRouter: TransactionsRouter + + private var backEnabled = true + + private var onBackPressedSubject: PublishSubject? = null + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_promotions) + toolbar() + onBackPressedSubject = PublishSubject.create() + transactionsRouter = TransactionsRouter() + supportFragmentManager.beginTransaction() + .add(R.id.fragment_container, PromotionsFragment.newInstance()) + .commit() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return if (item.itemId == android.R.id.home) { + if (backEnabled) { + transactionsRouter.open(this, true) + } else { + onBackPressedSubject?.onNext("") + } + true + } else { + super.onOptionsItemSelected(item) + } + } + + override fun navigateToGamification(bonus: Double) = + startActivity(GamificationActivity.newIntent(this, bonus)) + + override fun handleShare(link: String) { + ShareCompat.IntentBuilder.from(this) + .setText(link) + .setType("text/plain") + .setChooserTitle(resources.getString(R.string.referral_share_sheet_title)) + .startChooser() + } + + override fun openDetailsLink(url: String) { + try { + val uri = Uri.parse(url) + val launchBrowser = Intent(Intent.ACTION_VIEW, uri) + launchBrowser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(launchBrowser) + } catch (exception: ActivityNotFoundException) { + exception.printStackTrace() + Toast.makeText(this, R.string.unknown_error, Toast.LENGTH_SHORT) + .show() + } + } + + override fun navigateToInviteFriends() { + val intent = Intent(this, InviteFriendsActivity::class.java) + startActivity(intent) + } + + override fun backPressed() = onBackPressedSubject!! + + override fun enableBack() { + backEnabled = true + } + + override fun disableBack() { + backEnabled = false + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsActivityView.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsActivityView.kt new file mode 100644 index 00000000000..dbec0a084ee --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsActivityView.kt @@ -0,0 +1,20 @@ +package com.asfoundation.wallet.promotions + +import io.reactivex.Observable + +interface PromotionsActivityView { + + fun navigateToGamification(bonus: Double) + + fun navigateToInviteFriends() + + fun handleShare(link: String) + + fun openDetailsLink(url: String) + + fun backPressed(): Observable + + fun enableBack() + + fun disableBack() +} diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsAdapter.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsAdapter.kt new file mode 100644 index 00000000000..fd70eedca9b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsAdapter.kt @@ -0,0 +1,77 @@ +package com.asfoundation.wallet.promotions + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R +import io.reactivex.subjects.PublishSubject + + +class PromotionsAdapter(private val promotions: List, + private val clickListener: PublishSubject) : + RecyclerView.Adapter() { + + companion object { + private const val TITLE_VIEW_TYPE = 0 + private const val GAMIFICATION_VIEW_TYPE = 1 + private const val PROGRESS_VIEW_TYPE = 2 + private const val DEFAULT_VIEW_TYPE = 3 + private const val FUTURE_VIEW_TYPE = 4 + private const val REFERRAL_VIEW_TYPE = 5 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PromotionsViewHolder { + return when (viewType) { + GAMIFICATION_VIEW_TYPE -> { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.item_promotions_gamification, parent, false) + GamificationViewHolder(layout, clickListener) + } + PROGRESS_VIEW_TYPE -> { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.item_promotions_progress, parent, false) + ProgressViewHolder(layout, clickListener) + } + TITLE_VIEW_TYPE -> { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.item_promotions_title, parent, false) + TitleViewHolder(layout) + } + FUTURE_VIEW_TYPE -> { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.item_promotions_future, parent, false) + FutureViewHolder(layout, clickListener) + } + REFERRAL_VIEW_TYPE -> { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.item_promotions_referrals, parent, false) + ReferralViewHolder(layout, clickListener) + } + else -> { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.item_promotions_default, parent, false) + DefaultViewHolder(layout, clickListener) + } + } + } + + override fun getItemCount() = promotions.size + + override fun getItemViewType(position: Int): Int { + return when (promotions[position]) { + is GamificationItem -> GAMIFICATION_VIEW_TYPE + is TitleItem -> TITLE_VIEW_TYPE + is ProgressItem -> PROGRESS_VIEW_TYPE + is FutureItem -> FUTURE_VIEW_TYPE + is ReferralItem -> REFERRAL_VIEW_TYPE + else -> DEFAULT_VIEW_TYPE + } + } + + override fun onBindViewHolder(holder: PromotionsViewHolder, position: Int) { + holder.bind(promotions[position]) + } + +} + + diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsFragment.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsFragment.kt new file mode 100644 index 00000000000..95fe4e489a5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsFragment.kt @@ -0,0 +1,189 @@ +package com.asfoundation.wallet.promotions + +import android.content.Context +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.View.* +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.repository.SharedPreferencesRepository +import com.asfoundation.wallet.ui.gamification.GamificationMapper +import com.asfoundation.wallet.ui.widget.MarginItemDecoration +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.fragment_promotions.* +import kotlinx.android.synthetic.main.gamification_info_bottom_sheet.* +import kotlinx.android.synthetic.main.no_network_retry_only_layout.* +import javax.inject.Inject + +class PromotionsFragment : BasePageViewFragment(), PromotionsView { + + @Inject + lateinit var promotionsInteractor: PromotionsInteractorContract + + @Inject + lateinit var formatter: CurrencyFormatUtils + + @Inject + lateinit var mapper: GamificationMapper + + @Inject + lateinit var preferences: SharedPreferencesRepository + + private lateinit var activityView: PromotionsActivityView + private lateinit var presenter: PromotionsFragmentPresenter + private lateinit var adapter: PromotionsAdapter + private lateinit var detailsBottomSheet: BottomSheetBehavior + private var clickListener: PublishSubject? = null + private var onBackPressedSubject: PublishSubject? = null + + companion object { + fun newInstance() = PromotionsFragment() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + clickListener = PublishSubject.create() + onBackPressedSubject = PublishSubject.create() + presenter = + PromotionsFragmentPresenter(this, activityView, promotionsInteractor, preferences, + CompositeDisposable(), + Schedulers.io(), AndroidSchedulers.mainThread()) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + require( + context is PromotionsActivityView) { PromotionsFragment::class.java.simpleName + " needs to be attached to a " + PromotionsActivityView::class.java.simpleName } + activityView = context + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_promotions, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + detailsBottomSheet = BottomSheetBehavior.from(bottom_sheet_fragment_container) + detailsBottomSheet.addBottomSheetCallback( + object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) = Unit + + override fun onSlide(bottomSheet: View, slideOffset: Float) { + if (slideOffset == 0f) bottomsheet_coordinator_container.visibility = GONE + bottomsheet_coordinator_container.background.alpha = (255 * slideOffset).toInt() + } + }) + presenter.present() + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + override fun showPromotions(promotionsModel: PromotionsModel) { + adapter = PromotionsAdapter(promotionsModel.promotions, clickListener!!) + rv_promotions.addItemDecoration( + MarginItemDecoration(resources.getDimension(R.dimen.promotions_item_margin) + .toInt())) + rv_promotions.visibility = VISIBLE + rv_promotions.adapter = adapter + } + + override fun showLoading() { + promotions_progress_bar.visibility = VISIBLE + } + + override fun retryClick() = RxView.clicks(retry_button) + + override fun getPromotionClicks() = clickListener!! + + override fun showNetworkErrorView() { + no_promotions.visibility = GONE + no_network.visibility = VISIBLE + retry_button.visibility = VISIBLE + retry_animation.visibility = GONE + } + + override fun hideNetworkErrorView() { + no_network.visibility = GONE + } + + override fun showNoPromotionsScreen() { + no_network.visibility = GONE + retry_animation.visibility = GONE + no_promotions.visibility = VISIBLE + } + + override fun showRetryAnimation() { + retry_button.visibility = INVISIBLE + retry_animation.visibility = VISIBLE + } + + override fun hideLoading() { + promotions_progress_bar.visibility = INVISIBLE + } + + override fun hidePromotions() { + rv_promotions.visibility = GONE + } + + override fun getHomeBackPressed() = activityView.backPressed() + + override fun handleBackPressed() { + // Currently we only call the hide bottom sheet + // but maybe later additional stuff needs to be handled + hideBottomSheet() + } + + override fun getBottomSheetButtonClick() = RxView.clicks(got_it_button) + + override fun getBackPressed() = onBackPressedSubject!! + + override fun hideBottomSheet() { + detailsBottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + disableBackListener(bottomsheet_coordinator_container) + } + + override fun showBottomSheet() { + detailsBottomSheet.state = BottomSheetBehavior.STATE_EXPANDED + bottomsheet_coordinator_container.visibility = VISIBLE + bottomsheet_coordinator_container.background.alpha = 255 + setBackListener(bottomsheet_coordinator_container) + } + + override fun getBottomSheetContainerClick() = RxView.clicks(bottomsheet_coordinator_container) + + private fun setBackListener(view: View) { + activityView.disableBack() + view.apply { + isFocusableInTouchMode = true + requestFocus() + setOnKeyListener { _, keyCode, keyEvent -> + if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) { + if (detailsBottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) + onBackPressedSubject?.onNext("") + } + true + } + } + } + + private fun disableBackListener(view: View) { + activityView.enableBack() + view.apply { + isFocusableInTouchMode = false + setOnKeyListener(null) + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsFragmentPresenter.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsFragmentPresenter.kt new file mode 100644 index 00000000000..89d4ae3dbe5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsFragmentPresenter.kt @@ -0,0 +1,129 @@ +package com.asfoundation.wallet.promotions + +import com.appcoins.wallet.gamification.repository.entity.Status +import com.asfoundation.wallet.promotions.PromotionsInteractor.Companion.GAMIFICATION_ID +import com.asfoundation.wallet.promotions.PromotionsInteractor.Companion.GAMIFICATION_INFO +import com.asfoundation.wallet.promotions.PromotionsInteractor.Companion.REFERRAL_ID +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.util.isNoNetworkException +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.TimeUnit + +class PromotionsFragmentPresenter( + private val view: PromotionsView, + private val activityView: PromotionsActivityView, + private val promotionsInteractor: PromotionsInteractorContract, + private val preferences: PreferencesRepositoryType, + private val disposables: CompositeDisposable, + private val networkScheduler: Scheduler, + private val viewScheduler: Scheduler) { + + private var cachedBonus = 0.0 + + fun present() { + retrievePromotions() + handlePromotionClicks() + handleRetryClick() + handleBottomSheetVisibility() + handleBackPress() + } + + private fun retrievePromotions() { + disposables.add(promotionsInteractor.retrievePromotions() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSubscribe { + view.hidePromotions() + view.hideNetworkErrorView() + view.showLoading() + } + .doOnSuccess { onPromotions(it) } + .subscribe({}, { handleError(it) })) + } + + private fun onPromotions(promotionsModel: PromotionsModel) { + view.hideLoading() + when { + promotionsModel.error == Status.NO_NETWORK -> view.showNetworkErrorView() + promotionsModel.promotions.isNotEmpty() -> { + cachedBonus = promotionsModel.maxBonus + view.showPromotions(promotionsModel) + if (preferences.showGamificationDisclaimer()) { + view.showBottomSheet() + preferences.setGamificationDisclaimerShown() + } + } + else -> view.showNoPromotionsScreen() + } + } + + private fun handleError(throwable: Throwable) { + throwable.printStackTrace() + view.hideLoading() + if (throwable.isNoNetworkException()) view.showNetworkErrorView() + } + + private fun handleRetryClick() { + disposables.add(view.retryClick() + .observeOn(viewScheduler) + .doOnNext { view.showRetryAnimation() } + .delay(1, TimeUnit.SECONDS) + .observeOn(viewScheduler) + .doOnNext { retrievePromotions() } + .subscribe({}, { handleError(it) })) + } + + private fun handlePromotionClicks() { + disposables.add(view.getPromotionClicks() + .observeOn(viewScheduler) + .doOnNext { mapClickType(it) } + .subscribe({}, { handleError(it) })) + } + + private fun mapClickType(promotionClick: PromotionClick) { + when (promotionClick.id) { + GAMIFICATION_ID -> activityView.navigateToGamification(cachedBonus) + GAMIFICATION_INFO -> view.showBottomSheet() + REFERRAL_ID -> mapReferralClick(promotionClick.extras) + else -> mapPackagePerkClick(promotionClick.extras) + } + } + + private fun mapReferralClick(extras: Map?) { + if (extras != null) { + + val link = extras[ReferralViewHolder.KEY_LINK] + if (extras[ReferralViewHolder.KEY_ACTION] == ReferralViewHolder.ACTION_DETAILS) { + activityView.navigateToInviteFriends() + } else if (extras[ReferralViewHolder.KEY_ACTION] == ReferralViewHolder.ACTION_SHARE && link != null) { + activityView.handleShare(link) + } + } + } + + private fun mapPackagePerkClick(extras: Map?) { + if (extras != null && extras[PromotionsViewHolder.DETAILS_URL_EXTRA] != null) { + val detailsLink = extras[PromotionsViewHolder.DETAILS_URL_EXTRA] + activityView.openDetailsLink(detailsLink!!) + } + } + + fun stop() = disposables.clear() + + private fun handleBottomSheetVisibility() { + disposables.add(view.getBottomSheetButtonClick() + .mergeWith(view.getBottomSheetContainerClick()) + .observeOn(viewScheduler) + .doOnNext { view.hideBottomSheet() } + .subscribe({}, { handleError(it) })) + } + + private fun handleBackPress() { + disposables.add(Observable.merge(view.getBackPressed(), view.getHomeBackPressed()) + .observeOn(viewScheduler) + .doOnNext { view.handleBackPressed() } + .subscribe({}, { it.printStackTrace() })) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsGamificationAdapter.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsGamificationAdapter.kt new file mode 100644 index 00000000000..5c6e190ef72 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsGamificationAdapter.kt @@ -0,0 +1,28 @@ +package com.asfoundation.wallet.promotions + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R + + +class PromotionsGamificationAdapter(private val links: List) : + RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, + viewType: Int): PromotionsGamificationViewHolder { + + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.item_gamification_link, parent, false) + return PromotionsGamificationViewHolder(layout) + } + + override fun getItemCount() = links.size + + override fun onBindViewHolder(holder: PromotionsGamificationViewHolder, position: Int) { + holder.bind(links[position]) + } + +} + + diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsGamificationViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsGamificationViewHolder.kt new file mode 100644 index 00000000000..cfe51f3b8c3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsGamificationViewHolder.kt @@ -0,0 +1,22 @@ +package com.asfoundation.wallet.promotions + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R +import com.asfoundation.wallet.GlideApp +import kotlinx.android.synthetic.main.item_gamification_link.view.* + + +class PromotionsGamificationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + fun bind(gamificationLinkItem: GamificationLinkItem) { + GlideApp.with(itemView.context) + .load(gamificationLinkItem.icon) + .error(R.drawable.ic_promotions_default) + .circleCrop() + .into(itemView.link_icon) + + itemView.link_description.text = gamificationLinkItem.description + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsInteractor.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsInteractor.kt new file mode 100644 index 00000000000..66d79c9cd74 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsInteractor.kt @@ -0,0 +1,256 @@ +package com.asfoundation.wallet.promotions + +import com.appcoins.wallet.gamification.GamificationScreen +import com.appcoins.wallet.gamification.repository.Levels +import com.appcoins.wallet.gamification.repository.PromotionsRepository +import com.appcoins.wallet.gamification.repository.entity.* +import com.asf.wallet.R +import com.asfoundation.wallet.interact.EmptyNotification +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.referrals.ReferralInteractorContract +import com.asfoundation.wallet.referrals.ReferralsScreen +import com.asfoundation.wallet.ui.gamification.GamificationInteractor +import com.asfoundation.wallet.ui.gamification.GamificationMapper +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.functions.BiFunction +import io.reactivex.functions.Function3 +import java.util.concurrent.TimeUnit + +class PromotionsInteractor(private val referralInteractor: ReferralInteractorContract, + private val gamificationInteractor: GamificationInteractor, + private val promotionsRepo: PromotionsRepository, + private val findWalletInteract: FindDefaultWalletInteract, + private val mapper: GamificationMapper) : + PromotionsInteractorContract { + + companion object { + const val GAMIFICATION_ID = "GAMIFICATION" + const val GAMIFICATION_INFO = "GAMIFICATION_INFO" + const val REFERRAL_ID = "REFERRAL" + const val PROGRESS_VIEW_TYPE = "PROGRESS" + } + + override fun retrievePromotions(): Single { + return findWalletInteract.find() + .flatMap { + Single.zip( + gamificationInteractor.getLevels(), + promotionsRepo.getUserStatus(it.address), + BiFunction { level: Levels, userStatsResponse: UserStatusResponse -> + mapToPromotionsModel(userStatsResponse, level) + }) + .doOnSuccess { model -> + setSeenGenericPromotion(model.promotions, it.address, + PromotionUpdateScreen.PROMOTIONS) + } + } + + } + + override fun hasAnyPromotionUpdate(referralsScreen: ReferralsScreen, + gamificationScreen: GamificationScreen, + promotionUpdateScreen: PromotionUpdateScreen): Single { + return findWalletInteract.find() + .flatMap { wallet -> + promotionsRepo.getUserStatus(wallet.address) + .flatMap { + val gamification = + it.promotions.firstOrNull { promotionsResponse -> promotionsResponse is GamificationResponse } as GamificationResponse? + val referral = + it.promotions.firstOrNull { referralResponse -> referralResponse is ReferralResponse } as ReferralResponse? + val generic = it.promotions.filterIsInstance() + Single.zip( + referralInteractor.hasReferralUpdate(wallet.address, referral, + ReferralsScreen.PROMOTIONS), + gamificationInteractor.hasNewLevel(wallet.address, gamification, + GamificationScreen.PROMOTIONS), + hasGenericUpdate(generic, promotionUpdateScreen), + Function3 { hasReferralUpdate: Boolean, hasNewLevel: Boolean, hasGenericUpdate: Boolean -> + hasReferralUpdate || hasNewLevel || hasGenericUpdate + }) + } + } + } + + override fun getUnwatchedPromotionNotification(): Single { + return findWalletInteract.find() + .flatMap { wallet -> + promotionsRepo.getUserStatus(wallet.address) + .map { + val promotionList = it.promotions.filterIsInstance() + val unwatchedPromotion = getUnWatchedPromotion(promotionList) + buildPromotionNotification(unwatchedPromotion) + } + } + } + + private fun getUnWatchedPromotion(promotionList: List): GenericResponse? { + return promotionList.sortedByDescending { list -> list.priority } + .firstOrNull { + !promotionsRepo.getSeenGenericPromotion( + getPromotionIdKey(it.id, it.startDate, it.endDate), + PromotionUpdateScreen.TRANSACTIONS.name) + } + } + + private fun buildPromotionNotification(unwatchedPromotion: GenericResponse?): CardNotification { + return unwatchedPromotion?.let { res -> + if (!isFuturePromotion(res)) { + val action = CardNotificationAction.DETAILS_URL.takeIf { res.detailsLink != null } + ?: CardNotificationAction.NONE + PromotionNotification(action, res.title, res.description, res.icon, + getPromotionIdKey(res.id, res.startDate, res.endDate), res.detailsLink) + } else { + EmptyNotification() + } + } ?: EmptyNotification() + } + + override fun dismissNotification(id: String): Completable { + return Completable.fromAction { + promotionsRepo.setSeenGenericPromotion(id, PromotionUpdateScreen.TRANSACTIONS.name) + } + } + + private fun hasGenericUpdate(promotions: List, + screen: PromotionUpdateScreen): Single { + return Single.create { + val hasUpdate = promotions.any { promotion -> + promotionsRepo.getSeenGenericPromotion( + getPromotionIdKey(promotion.id, promotion.startDate, promotion.endDate), + screen.name) + .not() + } + it.onSuccess(hasUpdate) + } + } + + private fun setSeenGenericPromotion(promotions: List, wallet: String, + screen: PromotionUpdateScreen) { + promotions.forEach { + when (it) { + is GamificationItem -> { + promotionsRepo.shownLevel(wallet, it.level, GamificationScreen.PROMOTIONS.name) + it.links.forEach { gamificationLinkItem -> + promotionsRepo.setSeenGenericPromotion( + getPromotionIdKey(gamificationLinkItem.id, gamificationLinkItem.startDate, + gamificationLinkItem.endDate), screen.name) + } + } + is PerkPromotion -> promotionsRepo.setSeenGenericPromotion( + getPromotionIdKey(it.id, it.startDate, it.endDate), screen.name) + } + } + } + + private fun mapToPromotionsModel(userStatus: UserStatusResponse, + levels: Levels): PromotionsModel { + var gamificationAvailable = false + var perksAvailable = false + val promotions = mutableListOf() + var maxBonus = 0.0 + userStatus.promotions.sortedByDescending { it.priority } + .forEach { + when (it) { + is GamificationResponse -> { + gamificationAvailable = it.status == PromotionsResponse.Status.ACTIVE + + if (levels.isActive) { + maxBonus = levels.list.maxBy { level -> level.bonus }?.bonus ?: 0.0 + } + + if (gamificationAvailable) { + promotions.add(0, + TitleItem(R.string.perks_gamif_title, R.string.perks_gamif_subtitle, true, + maxBonus.toString())) + promotions.add(1, mapToGamificationItem(it)) + } + } + is ReferralResponse -> { + if (it.status == PromotionsResponse.Status.ACTIVE) { + promotions.add(mapToReferralItem(it)) + } + } + is GenericResponse -> { + if (it.linkedPromotionId != GAMIFICATION_ID) { + perksAvailable = true + when { + isFuturePromotion(it) -> promotions.add(mapToFutureItem(it)) + it.viewType == PROGRESS_VIEW_TYPE -> promotions.add(mapToProgressItem(it)) + else -> promotions.add(mapToDefaultItem(it)) + } + } + if (isValidGamificationLink(it.linkedPromotionId, gamificationAvailable, + it.startDate ?: 0)) { + mapToGamificationLinkItem(promotions, it) + } + } + } + } + + if (perksAvailable) { + promotions.add(2, + TitleItem(R.string.perks_title, R.string.perks_body, false)) + } + + return PromotionsModel(promotions, maxBonus, userStatus.error) + } + + private fun mapToGamificationLinkItem(promotions: MutableList, + genericResponse: GenericResponse) { + val gamificationItem = promotions[1] as GamificationItem + gamificationItem.links.add( + GamificationLinkItem(genericResponse.id, genericResponse.description, genericResponse.icon, + genericResponse.startDate, genericResponse.endDate)) + } + + private fun mapToProgressItem(genericResponse: GenericResponse): ProgressItem { + return ProgressItem(genericResponse.id, genericResponse.description, genericResponse.icon, + genericResponse.startDate, genericResponse.endDate, genericResponse.currentProgress!!, + genericResponse.objectiveProgress, genericResponse.detailsLink) + } + + private fun mapToDefaultItem(genericResponse: GenericResponse): DefaultItem { + return DefaultItem(genericResponse.id, genericResponse.description, genericResponse.icon, + genericResponse.startDate, genericResponse.endDate, genericResponse.detailsLink) + } + + private fun mapToGamificationItem( + gamificationResponse: GamificationResponse): GamificationItem { + val currentLevelInfo = mapper.mapCurrentLevelInfo(gamificationResponse.level) + val toNextLevelAmount = + gamificationResponse.nextLevelAmount?.minus(gamificationResponse.totalSpend) + + return GamificationItem(gamificationResponse.id, currentLevelInfo.planet, + gamificationResponse.level, currentLevelInfo.levelColor, currentLevelInfo.title, + toNextLevelAmount, gamificationResponse.bonus, mutableListOf()) + } + + private fun mapToReferralItem(referralResponse: ReferralResponse): ReferralItem { + return ReferralItem(referralResponse.id, referralResponse.amount, referralResponse.currency, + referralResponse.link.orEmpty()) + } + + private fun mapToFutureItem(genericResponse: GenericResponse): FutureItem { + return FutureItem(genericResponse.id, genericResponse.description, genericResponse.icon, + genericResponse.startDate, genericResponse.endDate, genericResponse.detailsLink) + } + + private fun isValidGamificationLink(linkedPromotionId: String?, + gamificationAvailable: Boolean, startDate: Long): Boolean { + val currentTime = TimeUnit.SECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS) + return linkedPromotionId != null && linkedPromotionId == GAMIFICATION_ID && gamificationAvailable && startDate < currentTime + } + + private fun isFuturePromotion(genericResponse: GenericResponse): Boolean { + val currentTime = TimeUnit.SECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS) + return genericResponse.startDate ?: 0 > currentTime + } + + private fun getPromotionIdKey(id: String, startDate: Long?, endDate: Long): String { + return id + "_" + startDate + "_" + endDate + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsInteractorContract.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsInteractorContract.kt new file mode 100644 index 00000000000..610eb0f92b0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsInteractorContract.kt @@ -0,0 +1,20 @@ +package com.asfoundation.wallet.promotions + +import com.appcoins.wallet.gamification.GamificationScreen +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.referrals.ReferralsScreen +import io.reactivex.Completable +import io.reactivex.Single + +interface PromotionsInteractorContract { + + fun retrievePromotions(): Single + + fun hasAnyPromotionUpdate(referralsScreen: ReferralsScreen, + gamificationScreen: GamificationScreen, + promotionUpdateScreen: PromotionUpdateScreen): Single + + fun getUnwatchedPromotionNotification(): Single + + fun dismissNotification(id: String): Completable +} diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsModel.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsModel.kt new file mode 100644 index 00000000000..57e41b58fe0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsModel.kt @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.promotions + +import com.appcoins.wallet.gamification.repository.entity.Status + +data class PromotionsModel(val promotions: List, + val maxBonus: Double, + val error: Status? = null) + diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsView.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsView.kt new file mode 100644 index 00000000000..1617442ad84 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsView.kt @@ -0,0 +1,40 @@ +package com.asfoundation.wallet.promotions + +import io.reactivex.Observable + +interface PromotionsView { + + fun showNetworkErrorView() + + fun hideNetworkErrorView() + + fun retryClick(): Observable + + fun showRetryAnimation() + + fun hideLoading() + + fun showLoading() + + fun showNoPromotionsScreen() + + fun showPromotions(promotionsModel: PromotionsModel) + + fun hidePromotions() + + fun getPromotionClicks(): Observable + + fun getHomeBackPressed(): Observable + + fun handleBackPressed() + + fun getBottomSheetButtonClick(): Observable + + fun getBackPressed(): Observable + + fun hideBottomSheet() + + fun showBottomSheet() + + fun getBottomSheetContainerClick(): Observable +} diff --git a/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsViewHolder.kt new file mode 100644 index 00000000000..1d5f2e556df --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/promotions/PromotionsViewHolder.kt @@ -0,0 +1,259 @@ +package com.asfoundation.wallet.promotions + +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.PluralsRes +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R +import com.asfoundation.wallet.GlideApp +import com.asfoundation.wallet.promotions.PromotionsInteractor.Companion.GAMIFICATION_INFO +import com.asfoundation.wallet.ui.gamification.GamificationMapper +import com.asfoundation.wallet.ui.widget.MarginItemDecoration +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.item_promotions_default.view.* +import kotlinx.android.synthetic.main.item_promotions_future.view.* +import kotlinx.android.synthetic.main.item_promotions_gamification.view.* +import kotlinx.android.synthetic.main.item_promotions_progress.view.* +import kotlinx.android.synthetic.main.item_promotions_referrals.view.* +import kotlinx.android.synthetic.main.item_promotions_title.view.* +import java.math.BigDecimal +import java.text.DecimalFormat +import java.util.concurrent.TimeUnit + +abstract class PromotionsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + companion object { + const val DETAILS_URL_EXTRA = "DETAILS_URL_EXTRA" + } + + abstract fun bind(promotion: Promotion) + + protected fun handleExpiryDate(view: TextView, container: LinearLayout, endDate: Long) { + val currentTime = TimeUnit.SECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS) + val diff: Long = endDate - currentTime + val days = TimeUnit.DAYS.convert(diff, TimeUnit.SECONDS) + val hours = TimeUnit.HOURS.convert(diff, TimeUnit.SECONDS) + val minutes = TimeUnit.MINUTES.convert(diff, TimeUnit.SECONDS) + + when { + days > 3 -> container.visibility = View.GONE + days in 1..3 -> updateDate(view, container, days, R.plurals.promotion_ends) + hours > 0 -> updateDate(view, container, hours, R.plurals.promotion_ends_hours) + else -> updateDate(view, container, minutes, R.plurals.promotion_ends_minutes) + } + } + + private fun updateDate(view: TextView, container: LinearLayout, time: Long, + @PluralsRes text: Int) { + container.visibility = View.VISIBLE + view.text = + itemView.context.resources.getQuantityString(text, time.toInt(), time.toString()) + } + +} + +class TitleViewHolder(itemView: View) : PromotionsViewHolder(itemView) { + + override fun bind(promotion: Promotion) { + val titleItem = promotion as TitleItem + + val title = if (titleItem.isGamificationTitle) { + val formatter = CurrencyFormatUtils.create() + val bonus = formatter.formatGamificationValues(BigDecimal(titleItem.bonus)) + itemView.context.getString(titleItem.title, bonus) + } else itemView.context.getString(titleItem.title) + itemView.promotions_title.text = title + itemView.promotions_subtitle.setText(titleItem.subtitle) + } + +} + +class ProgressViewHolder(itemView: View, + private val clickListener: PublishSubject) : + PromotionsViewHolder(itemView) { + + override fun bind(promotion: Promotion) { + val progressItem = promotion as ProgressItem + + itemView.isClickable = progressItem.detailsLink != null + + itemView.setOnClickListener { + val extras = emptyMap().toMutableMap() + progressItem.detailsLink?.let { + extras[DETAILS_URL_EXTRA] = it + } + clickListener.onNext(PromotionClick(promotion.id, extras)) + } + + GlideApp.with(itemView.context) + .load(progressItem.icon) + .error(R.drawable.ic_promotions_default) + .circleCrop() + .into(itemView.progress_icon) + + itemView.progress_title.text = progressItem.description + if (progressItem.objective != null) { + itemView.progress_current.max = progressItem.objective.toInt() + itemView.progress_current.progress = progressItem.current.toInt() + val progress = "${progressItem.current.toInt()}/${progressItem.objective.toInt()}" + itemView.progress_label.text = progress + } else { + itemView.progress_current.max = progressItem.current.toInt() + itemView.progress_current.progress = progressItem.current.toInt() + itemView.progress_label.text = "${progressItem.current.toInt()}" + } + handleExpiryDate(itemView.progress_expiry_date, itemView.progress_container_date, + progressItem.endDate) + } + +} + +class DefaultViewHolder(itemView: View, + private val clickListener: PublishSubject) : + PromotionsViewHolder(itemView) { + + override fun bind(promotion: Promotion) { + val defaultItem = promotion as DefaultItem + + itemView.isClickable = defaultItem.detailsLink != null + + itemView.setOnClickListener { + val extras = emptyMap().toMutableMap() + defaultItem.detailsLink?.let { + extras[DETAILS_URL_EXTRA] = it + } + clickListener.onNext(PromotionClick(promotion.id, extras)) + } + + GlideApp.with(itemView.context) + .load(defaultItem.icon) + .error(R.drawable.ic_promotions_default) + .circleCrop() + .into(itemView.default_icon) + + itemView.default_title.text = defaultItem.description + handleExpiryDate(itemView.default_expiry_date, itemView.default_container_date, + defaultItem.endDate) + } + +} + +class FutureViewHolder(itemView: View, + private val clickListener: PublishSubject) : + PromotionsViewHolder(itemView) { + + override fun bind(promotion: Promotion) { + val futureItem = promotion as FutureItem + + itemView.isClickable = futureItem.detailsLink != null + + itemView.setOnClickListener { + val extras = emptyMap().toMutableMap() + futureItem.detailsLink?.let { + extras[DETAILS_URL_EXTRA] = it + } + clickListener.onNext(PromotionClick(promotion.id, extras)) + } + + GlideApp.with(itemView.context) + .load(futureItem.icon) + .error(R.drawable.ic_promotions_default) + .circleCrop() + .into(itemView.future_icon) + + itemView.future_title.text = futureItem.description + } + +} + +class ReferralViewHolder(itemView: View, + private val clickListener: PublishSubject) : + PromotionsViewHolder(itemView) { + + companion object { + const val KEY_ACTION = "ACTION" + const val KEY_LINK = "LINK" + const val ACTION_DETAILS = "DETAILS" + const val ACTION_SHARE = "SHARE" + } + + override fun bind(promotion: Promotion) { + val referralItem = promotion as ReferralItem + + itemView.setOnClickListener { + val extras = mapOf( + Pair(KEY_LINK, referralItem.link), + Pair(KEY_ACTION, ACTION_DETAILS) + ) + clickListener.onNext(PromotionClick(promotion.id, extras)) + } + + itemView.share_container.setOnClickListener { + val extras = mapOf( + Pair(KEY_LINK, referralItem.link), + Pair(KEY_ACTION, ACTION_SHARE) + ) + clickListener.onNext(PromotionClick(promotion.id, extras)) + } + + val formatter = CurrencyFormatUtils.create() + val bonus = formatter.formatCurrency(referralItem.bonus, WalletCurrency.FIAT) + + val subtitle = itemView.context.getString(R.string.promotions_referral_card_title, + referralItem.currency + bonus) + + itemView.referral_subtitle.text = subtitle + } + +} + +class GamificationViewHolder(itemView: View, + private val clickListener: PublishSubject) : + PromotionsViewHolder(itemView) { + + private var mapper = GamificationMapper(itemView.context) + + override fun bind(promotion: Promotion) { + val gamificationItem = promotion as GamificationItem + val formatter = CurrencyFormatUtils.create() + val df = DecimalFormat("###.#") + + itemView.setOnClickListener { + clickListener.onNext(PromotionClick(promotion.id)) + } + + itemView.planet.setImageDrawable(gamificationItem.planet) + itemView.current_level_bonus.background = mapper.getOvalBackground(gamificationItem.levelColor) + itemView.current_level_bonus.text = + itemView.context?.getString(R.string.gamif_bonus, df.format(gamificationItem.bonus)) + itemView.planet_title.text = gamificationItem.title + if (gamificationItem.toNextLevelAmount != null) { + itemView.planet_subtitle.text = itemView.context.getString(R.string.gamif_card_body, + formatter.formatGamificationValues(gamificationItem.toNextLevelAmount)) + } else { + itemView.planet_subtitle.visibility = View.INVISIBLE + } + + itemView.gamification_info_btn.setOnClickListener { + clickListener.onNext(PromotionClick(GAMIFICATION_INFO)) + } + + handleLinks(gamificationItem.links, itemView) + } + + private fun handleLinks(links: List, itemView: View) { + if (links.isEmpty()) { + itemView.linked_perks.visibility = View.GONE + } else { + itemView.linked_perks.visibility = View.VISIBLE + val adapter = PromotionsGamificationAdapter(links) + itemView.linked_perks.addItemDecoration( + MarginItemDecoration(itemView.resources.getDimension(R.dimen.promotions_item_margin) + .toInt())) + itemView.linked_perks.adapter = adapter + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/CardNotification.kt b/app/src/main/java/com/asfoundation/wallet/referrals/CardNotification.kt new file mode 100644 index 00000000000..224030ff5dc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/CardNotification.kt @@ -0,0 +1,11 @@ +package com.asfoundation.wallet.referrals + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction + +open class CardNotification(@StringRes open val title: Int? = null, + @StringRes open val body: Int? = null, + @DrawableRes open val icon: Int? = null, + @StringRes open val positiveButtonText: Int? = null, + open val positiveAction: CardNotificationAction) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsActivity.kt b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsActivity.kt new file mode 100644 index 00000000000..959c13a1630 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsActivity.kt @@ -0,0 +1,166 @@ +package com.asfoundation.wallet.referrals + +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View.* +import androidx.core.app.ShareCompat +import androidx.fragment.app.Fragment +import com.asf.wallet.R +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.router.ExternalBrowserRouter +import com.asfoundation.wallet.ui.BaseActivity +import com.asfoundation.wallet.wallet_validation.generic.WalletValidationActivity +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.AndroidInjection +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.ReplaySubject +import kotlinx.android.synthetic.main.invite_friends_activity_layout.* +import kotlinx.android.synthetic.main.no_network_retry_only_layout.* +import java.math.BigDecimal +import javax.inject.Inject + +class InviteFriendsActivity : BaseActivity(), InviteFriendsActivityView { + + private lateinit var menu: Menu + private lateinit var presenter: InviteFriendsActivityPresenter + private lateinit var browserRouter: ExternalBrowserRouter + private var infoButtonSubject: PublishSubject? = null + private var infoButtonInitializeSubject: ReplaySubject? = null + @Inject + lateinit var referralInteractor: ReferralInteractorContract + @Inject + lateinit var walletInteract: FindDefaultWalletInteract + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + AndroidInjection.inject(this) + setContentView(R.layout.invite_friends_activity_layout) + toolbar() + infoButtonSubject = PublishSubject.create() + infoButtonInitializeSubject = ReplaySubject.create() + browserRouter = ExternalBrowserRouter() + navigateTo(LoadingFragment()) + presenter = + InviteFriendsActivityPresenter(this, referralInteractor, walletInteract, + CompositeDisposable(), Schedulers.io(), AndroidSchedulers.mainThread()) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + super.onBackPressed() + } + R.id.action_info -> { + infoButtonSubject?.onNext(Any()) + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_info, menu) + this.menu = menu + infoButtonInitializeSubject?.onNext(true) + return super.onCreateOptionsMenu(menu) + } + + override fun onResume() { + super.onResume() + presenter.present() + } + + override fun navigateToVerificationFragment(amount: BigDecimal, currency: String) { + hideNoNetworkView() + navigateTo(InviteFriendsVerificationFragment.newInstance(amount, currency)) + } + + override fun navigateToInviteFriends(amount: BigDecimal, pendingAmount: BigDecimal, + currency: String, link: String?, completed: Int, + receivedAmount: BigDecimal, maxAmount: BigDecimal, + available: Int, isRedeemed: Boolean) { + hideNoNetworkView() + navigateTo(InviteFriendsFragment.newInstance(amount, pendingAmount, currency, link, completed, + receivedAmount, maxAmount, available, isRedeemed)) + } + + override fun getInfoButtonClick(): Observable { + return infoButtonSubject!! + } + + override fun infoButtonInitialized(): Observable { + return infoButtonInitializeSubject!! + } + + override fun showInfoButton() { + this.menu.findItem(R.id.action_info) + .isVisible = true + } + + override fun navigateToWalletValidation(beenInvited: Boolean) { + startActivity(WalletValidationActivity.newIntent(this, beenInvited, + navigateToTransactionsOnSuccess = false, + navigateToTransactionsOnCancel = false, + showToolbar = true, previousContext = "invite_friends")) + } + + override fun navigateToTopApps() { + browserRouter.open(this, Uri.parse(APTOIDE_TOP_APPS_URL)) + } + + override fun showShare(link: String) { + ShareCompat.IntentBuilder.from(this) + .setText(link) + .setType("text/plain") + .setChooserTitle(resources.getString(R.string.referral_share_sheet_title)) + .startChooser() + } + + private fun hideNoNetworkView() { + fragment_container.visibility = VISIBLE + no_network.visibility = GONE + } + + private fun navigateTo(fragment: Fragment) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, fragment) + .commit() + } + + override fun retryClick(): Observable { + return RxView.clicks(retry_button) + } + + override fun showNetworkErrorView() { + no_network.visibility = VISIBLE + retry_button.visibility = VISIBLE + retry_animation.visibility = GONE + fragment_container.visibility = GONE + } + + override fun showRetryAnimation() { + retry_button.visibility = INVISIBLE + retry_animation.visibility = VISIBLE + } + + override fun onPause() { + presenter.stop() + super.onPause() + } + + override fun onDestroy() { + infoButtonSubject = null + infoButtonInitializeSubject = null + super.onDestroy() + } + + companion object { + const val APTOIDE_TOP_APPS_URL = "https://en.aptoide.com/store/bds-store/group/group-10867" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsActivityPresenter.kt b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsActivityPresenter.kt new file mode 100644 index 00000000000..4a259421e64 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsActivityPresenter.kt @@ -0,0 +1,72 @@ +package com.asfoundation.wallet.referrals + +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.util.isNoNetworkException +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.TimeUnit + +class InviteFriendsActivityPresenter(private val activity: InviteFriendsActivityView, + private val referralInteractor: ReferralInteractorContract, + private val walletInteract: FindDefaultWalletInteract, + private val disposables: CompositeDisposable, + private val networkScheduler: Scheduler, + private val viewScheduler: Scheduler) { + + fun present() { + handleFragmentNavigation() + handleRetryClick() + } + + private fun handleFragmentNavigation() { + disposables.add(walletInteract.find() + .flatMap { referralInteractor.retrieveReferral() } + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { handleValidationResult(it) } + .flatMapCompletable { + referralInteractor.saveReferralInformation(it.completed, it.link != null, + ReferralsScreen.INVITE_FRIENDS) + } + .subscribe({}, { handleError(it) }) + ) + } + + private fun handleValidationResult(referral: ReferralModel) { + if (referral.link != null) { + activity.navigateToInviteFriends(referral.amount, referral.pendingAmount, + referral.symbol, referral.link, referral.completed, referral.receivedAmount, + referral.maxAmount, referral.available, referral.isRedeemed) + handleInfoButtonVisibility() + } else { + activity.navigateToVerificationFragment(referral.amount, referral.symbol) + } + } + + private fun handleInfoButtonVisibility() { + disposables.add(activity.infoButtonInitialized() + .filter { it } + .doOnNext { activity.showInfoButton() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleError(throwable: Throwable) { + throwable.printStackTrace() + if (throwable.isNoNetworkException()) { + activity.showNetworkErrorView() + } + } + + private fun handleRetryClick() { + disposables.add(activity.retryClick() + .observeOn(viewScheduler) + .doOnNext { activity.showRetryAnimation() } + .delay(1, TimeUnit.SECONDS) + .doOnNext { handleFragmentNavigation() } + .subscribe({}, { it.printStackTrace() })) + } + + fun stop() { + disposables.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsActivityView.kt b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsActivityView.kt new file mode 100644 index 00000000000..00188593ec1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsActivityView.kt @@ -0,0 +1,23 @@ +package com.asfoundation.wallet.referrals + +import io.reactivex.Observable +import java.math.BigDecimal + +interface InviteFriendsActivityView { + + fun navigateToVerificationFragment(amount: BigDecimal, currency: String) + + fun navigateToInviteFriends(amount: BigDecimal, pendingAmount: BigDecimal, currency: String, + link: String?, completed: Int, receivedAmount: BigDecimal, + maxAmount: BigDecimal, available: Int, isRedeemed: Boolean) + + fun getInfoButtonClick(): Observable + fun infoButtonInitialized(): Observable + fun showInfoButton() + fun navigateToWalletValidation(beenInvited: Boolean) + fun showShare(link: String) + fun navigateToTopApps() + fun showNetworkErrorView() + fun showRetryAnimation() + fun retryClick(): Observable +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsFragment.kt b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsFragment.kt new file mode 100644 index 00000000000..64b6ae61c34 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsFragment.kt @@ -0,0 +1,227 @@ +package com.asfoundation.wallet.referrals + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.util.scaleToString +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.invite_friends_fragment_layout.* +import kotlinx.android.synthetic.main.referral_notification_card.* +import java.math.BigDecimal +import javax.inject.Inject + +class InviteFriendsFragment : BasePageViewFragment(), InviteFriendsFragmentView { + + @Inject + lateinit var referralInteractor: ReferralInteractorContract + + private lateinit var presenter: InviteFriendsFragmentPresenter + private var activity: InviteFriendsActivityView? = null + private lateinit var referralsBottomSheet: BottomSheetBehavior + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = + InviteFriendsFragmentPresenter(this, activity, CompositeDisposable(), referralInteractor) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + require( + context is InviteFriendsActivityView) { InviteFriendsFragment::class.java.simpleName + " needs to be attached to a " + InviteFriendsActivity::class.java.simpleName } + activity = context + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + referralsBottomSheet = + BottomSheetBehavior.from(bottom_sheet_fragment_container) + animateBackgroundFade() + setTextValues() + presenter.present() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + childFragmentManager.beginTransaction() + .replace(R.id.bottom_sheet_fragment_container, + ReferralsFragment.newInstance(amount, pendingAmount, currency, completedInvites, + receivedAmount, maxAmount, available, isRedeemed)) + .commit() + return inflater.inflate(R.layout.invite_friends_fragment_layout, container, false) + } + + private fun animateBackgroundFade() { + referralsBottomSheet.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) { + background_fade_animation?.progress = slideOffset + } + }) + } + + private fun setTextValues() { + referral_description.text = + getString(R.string.referral_view_verified_body, + currency + amount.scaleToString(2)) + notification_title.text = + getString(R.string.referral_notification_bonus_pending_title, + currency + pendingAmount.scaleToString(2)) + } + + override fun shareLinkClick(): Observable { + return RxView.clicks(share_invite_button) + } + + override fun appsAndGamesButtonClick(): Observable { + return RxView.clicks(notification_apps_games_button) + } + + override fun showShare() { + activity?.showShare(link) + } + + override fun navigateToAptoide() { + activity?.navigateToTopApps() + } + + override fun showNotificationCard(pendingAmount: BigDecimal, symbol: String, + icon: Int?) { + if (pendingAmount.toDouble() > 0) { + icon?.let { notification_image.setImageResource(icon) } + notification_title.text = getString(R.string.referral_notification_bonus_pending_title, + "$symbol${pendingAmount.scaleToString(2)}") + referral_notification_card.visibility = VISIBLE + } else { + referral_notification_card.visibility = GONE + } + } + + override fun changeBottomSheetState() { + if (referralsBottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) { + referralsBottomSheet.state = BottomSheetBehavior.STATE_EXPANDED + } else if (referralsBottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) { + referralsBottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + } + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + private val receivedAmount: BigDecimal by lazy { + if (arguments!!.containsKey(RECEIVED_AMOUNT)) { + arguments!!.getSerializable(RECEIVED_AMOUNT) as BigDecimal + } else { + throw IllegalArgumentException("Received amount not found") + } + } + + private val maxAmount: BigDecimal by lazy { + if (arguments!!.containsKey(MAX_AMOUNT)) { + arguments!!.getSerializable(MAX_AMOUNT) as BigDecimal + } else { + throw IllegalArgumentException("Max amount not found") + } + } + + private val amount: BigDecimal by lazy { + if (arguments!!.containsKey(AMOUNT)) { + arguments!!.getSerializable(AMOUNT) as BigDecimal + } else { + throw IllegalArgumentException("Amount not found") + } + } + + private val pendingAmount: BigDecimal by lazy { + if (arguments!!.containsKey(PENDING_AMOUNT)) { + arguments!!.getSerializable(PENDING_AMOUNT) as BigDecimal + } else { + throw IllegalArgumentException("Pending amount not found") + } + } + + private val currency: String by lazy { + if (arguments!!.containsKey(CURRENCY)) { + arguments!!.getString(CURRENCY, "") + } else { + throw IllegalArgumentException("Currency not found") + } + } + + private val link: String by lazy { + if (arguments!!.containsKey(LINK)) { + arguments!!.getString(LINK, "") + } else { + throw IllegalArgumentException("link not found") + } + } + + private val completedInvites: Int by lazy { + if (arguments!!.containsKey(COMPLETED_INVITES)) { + arguments!!.getInt(COMPLETED_INVITES) + } else { + throw IllegalArgumentException("Completed not found") + } + } + + private val available: Int by lazy { + if (arguments!!.containsKey(AVAILABLE)) { + arguments!!.getInt(AVAILABLE) + } else { + throw IllegalArgumentException("available not found") + } + } + + private val isRedeemed: Boolean by lazy { + if (arguments!!.containsKey(IS_REDEEMED)) { + arguments!!.getBoolean(IS_REDEEMED) + } else { + throw IllegalArgumentException("is redeemed not found") + } + } + + companion object { + + private const val AMOUNT = "amount" + private const val PENDING_AMOUNT = "pending_amount" + private const val LINK = "link" + private const val COMPLETED_INVITES = "completed_invites" + private const val RECEIVED_AMOUNT = "received_amount" + private const val MAX_AMOUNT = "max_amount" + private const val AVAILABLE = "available" + private const val CURRENCY = "currency" + private const val IS_REDEEMED = "is_redeemed" + + fun newInstance(amount: BigDecimal, pendingAmount: BigDecimal, currency: String, link: String?, + completed: Int, receivedAmount: BigDecimal, maxAmount: BigDecimal, + available: Int, isRedeemed: Boolean): InviteFriendsFragment { + val bundle = Bundle().apply { + putSerializable(AMOUNT, amount) + putSerializable(PENDING_AMOUNT, pendingAmount) + putString(CURRENCY, currency) + putString(LINK, link) + putInt(COMPLETED_INVITES, completed) + putSerializable(RECEIVED_AMOUNT, receivedAmount) + putSerializable(MAX_AMOUNT, maxAmount) + putInt(AVAILABLE, available) + putBoolean(IS_REDEEMED, isRedeemed) + } + return InviteFriendsFragment().apply { + arguments = bundle + } + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsFragmentPresenter.kt b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsFragmentPresenter.kt new file mode 100644 index 00000000000..d0913b7a49f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsFragmentPresenter.kt @@ -0,0 +1,63 @@ +package com.asfoundation.wallet.referrals + +import com.asfoundation.wallet.util.isNoNetworkException +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import java.math.BigDecimal + +class InviteFriendsFragmentPresenter(private val view: InviteFriendsFragmentView, + private val activity: InviteFriendsActivityView?, + private val disposable: CompositeDisposable, + private val referralInteractor: ReferralInteractorContract) { + + fun present() { + handleInfoButtonClick() + handleShareClicks() + handleAppsGamesClicks() + handlePendingNotification() + } + + private fun handlePendingNotification() { + disposable.add( + referralInteractor.getPendingBonusNotification() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess { view.showNotificationCard(it.pendingAmount, it.symbol, it.icon) } + .doOnComplete { view.showNotificationCard(BigDecimal.ZERO, "", null) } + .doOnError { handlerError(it) } + .subscribe() + ) + } + + private fun handleShareClicks() { + disposable.add(view.shareLinkClick() + .doOnNext { view.showShare() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleAppsGamesClicks() { + disposable.add(view.appsAndGamesButtonClick() + .doOnNext { view.navigateToAptoide() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleInfoButtonClick() { + activity?.let { + disposable.add(it.getInfoButtonClick() + .doOnNext { view.changeBottomSheetState() } + .subscribe()) + } + } + + private fun handlerError(throwable: Throwable) { + throwable.printStackTrace() + if (throwable.isNoNetworkException()) { + activity?.showNetworkErrorView() + } + } + + fun stop() { + disposable.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsFragmentView.kt b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsFragmentView.kt new file mode 100644 index 00000000000..328225870f0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsFragmentView.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.referrals + +import io.reactivex.Observable +import java.math.BigDecimal + +interface InviteFriendsFragmentView { + fun shareLinkClick(): Observable + fun appsAndGamesButtonClick(): Observable + fun showShare() + fun navigateToAptoide() + fun showNotificationCard(pendingAmount: BigDecimal, symbol: String, icon: Int?) + fun changeBottomSheetState() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsVerificationFragment.kt b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsVerificationFragment.kt new file mode 100644 index 00000000000..fce20e3ed86 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsVerificationFragment.kt @@ -0,0 +1,98 @@ +package com.asfoundation.wallet.referrals + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.invite_friends_verification_layout.* +import java.math.BigDecimal +import javax.inject.Inject + +class InviteFriendsVerificationFragment : BasePageViewFragment(), InviteFriendsVerificationView { + + @Inject + lateinit var formatter: CurrencyFormatUtils + private lateinit var presenter: InviteFriendsVerificationPresenter + private lateinit var activity: InviteFriendsActivityView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = InviteFriendsVerificationPresenter(this, CompositeDisposable()) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + require( + context is InviteFriendsActivityView) { InviteFriendsVerificationFragment::class.java.simpleName + " needs to be attached to a " + InviteFriendsActivity::class.java.simpleName } + activity = context + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setDescriptionText() + presenter.present() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.invite_friends_verification_layout, container, false) + } + + private fun setDescriptionText() { + val formattedAmount = formatter.formatCurrency(amount, WalletCurrency.FIAT) + verification_description.text = getString(R.string.referral_view_unverified_body, + currency.plus(formattedAmount)) + } + + override fun verifyButtonClick() = RxView.clicks(verify_button) + + override fun beenInvitedClick() = RxView.clicks(invited_button) + + override fun navigateToWalletValidation(beenInvited: Boolean) { + activity.navigateToWalletValidation(beenInvited) + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + val amount: BigDecimal by lazy { + if (arguments!!.containsKey(AMOUNT)) { + arguments!!.getSerializable(AMOUNT) as BigDecimal + } else { + throw IllegalArgumentException("Amount not found") + } + } + + val currency: String by lazy { + if (arguments!!.containsKey(CURRENCY)) { + arguments!!.getString(CURRENCY) + } else { + throw IllegalArgumentException("Currency not found") + } + } + + companion object { + + private const val AMOUNT = "amount" + private const val CURRENCY = "currency" + + fun newInstance(amount: BigDecimal, currency: String): InviteFriendsVerificationFragment { + val bundle = Bundle().apply { + putSerializable(AMOUNT, amount) + putString(CURRENCY, currency) + } + return InviteFriendsVerificationFragment().apply { + arguments = bundle + } + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsVerificationPresenter.kt b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsVerificationPresenter.kt new file mode 100644 index 00000000000..fb27e250ffc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsVerificationPresenter.kt @@ -0,0 +1,32 @@ +package com.asfoundation.wallet.referrals + +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.TimeUnit + +class InviteFriendsVerificationPresenter(private val view: InviteFriendsVerificationView, + private val disposable: CompositeDisposable) { + + fun present() { + handleVerifyClick() + handleBeenInvitedClick() + } + + private fun handleVerifyClick() { + disposable.add(view.verifyButtonClick() + .throttleFirst(1, TimeUnit.SECONDS) + .doOnNext { view.navigateToWalletValidation(false) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleBeenInvitedClick() { + disposable.add(view.beenInvitedClick() + .throttleFirst(1, TimeUnit.SECONDS) + .doOnNext { view.navigateToWalletValidation(true) } + .subscribe({}, { it.printStackTrace() })) + } + + fun stop() { + disposable.clear() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsVerificationView.kt b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsVerificationView.kt new file mode 100644 index 00000000000..2d3fb18df87 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/InviteFriendsVerificationView.kt @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.referrals + +import io.reactivex.Observable + +interface InviteFriendsVerificationView { + fun beenInvitedClick(): Observable + fun verifyButtonClick(): Observable + fun navigateToWalletValidation(beenInvited: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/LoadingFragment.kt b/app/src/main/java/com/asfoundation/wallet/referrals/LoadingFragment.kt new file mode 100644 index 00000000000..b4c8321c300 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/LoadingFragment.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.referrals + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.asf.wallet.R + +class LoadingFragment : Fragment() { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.generic_loading, container, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/ReferralInteractor.kt b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralInteractor.kt new file mode 100644 index 00000000000..d2451f74667 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralInteractor.kt @@ -0,0 +1,149 @@ +package com.asfoundation.wallet.referrals + +import com.appcoins.wallet.gamification.repository.PromotionsRepository +import com.appcoins.wallet.gamification.repository.entity.PromotionsResponse +import com.appcoins.wallet.gamification.repository.entity.ReferralResponse +import com.asf.wallet.R +import com.asfoundation.wallet.interact.EmptyNotification +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction +import com.asfoundation.wallet.util.scaleToString +import io.reactivex.Completable +import io.reactivex.Maybe +import io.reactivex.Single +import java.math.BigDecimal + +class ReferralInteractor( + private val preferences: SharedPreferencesReferralLocalData, + private val defaultWallet: FindDefaultWalletInteract, + private val promotionsRepository: PromotionsRepository) : + ReferralInteractorContract { + + override fun hasReferralUpdate(walletAddress: String, + referralResponse: ReferralResponse?, + screen: ReferralsScreen): Single { + return if (referralResponse == null || referralResponse.status != PromotionsResponse.Status.ACTIVE) { + Single.just(false) + } else { + getReferralInformation(walletAddress, screen) + .map { + val verified = referralResponse.link != null + hasDifferentInformation(referralResponse.completed.toString() + verified, it) + } + + } + } + + override fun retrieveReferral(): Single { + return defaultWallet.find() + .flatMap { promotionsRepository.getReferralUserStatus(it.address) } + .map { map(it) } + } + + private fun map(referralResponse: ReferralResponse): ReferralModel { + return ReferralModel(referralResponse.completed, referralResponse.link, + referralResponse.invited, referralResponse.pendingAmount, referralResponse.amount, + referralResponse.symbol, referralResponse.maxAmount, referralResponse.minAmount, + referralResponse.available, referralResponse.receivedAmount, + isRedeemed(referralResponse.userStatus), isAvailable(referralResponse.status)) + } + + override fun saveReferralInformation(numberOfFriends: Int, isVerified: Boolean, + screen: ReferralsScreen): Completable { + return defaultWallet.find() + .flatMapCompletable { + saveReferralInformation(it.address, numberOfFriends, isVerified, screen) + } + } + + private fun getReferralInformation(address: String, screen: ReferralsScreen): Single { + return preferences.getReferralInformation(address, screen.toString()) + } + + private fun saveReferralInformation(address: String, numberOfFriends: Int, isVerified: Boolean, + screen: ReferralsScreen): Completable { + return preferences.saveReferralInformation(address, numberOfFriends, isVerified, + screen.toString()) + } + + private fun hasDifferentInformation(newInformation: String, savedInformation: String): Boolean { + return newInformation != savedInformation + } + + override fun getPendingBonusNotification(): Maybe { + return defaultWallet.find() + .flatMapMaybe { wallet -> + promotionsRepository.getReferralUserStatus(wallet.address) + .map { mapResponse(it) } + .onErrorReturn { ReferralModel() } + .filter { it.pendingAmount.compareTo(BigDecimal.ZERO) != 0 && it.isActive } + .map { + ReferralNotification( + R.string.referral_notification_bonus_pending_title, + R.string.referral_notification_bonus_pending_body, + R.drawable.ic_bonus_pending, + R.string.gamification_APPCapps_button, + CardNotificationAction.DISCOVER, + it.pendingAmount, + it.symbol) + } + } + } + + override fun getUnwatchedPendingBonusNotification(): Single { + return defaultWallet.find() + .flatMap { wallet -> + promotionsRepository.getReferralUserStatus(wallet.address) + .map { mapResponse(it) } + .onErrorReturn { ReferralModel() } + .flatMap { referralModel -> + preferences.getPendingAmountNotification(wallet.address) + .map { + referralModel.pendingAmount.compareTo(BigDecimal.ZERO) != 0 && + it != referralModel.pendingAmount.scaleToString( + 2) && referralModel.isActive + } + .map { shouldShow -> + ReferralNotification( + R.string.referral_notification_bonus_pending_title, + R.string.referral_notification_bonus_pending_body, + R.drawable.ic_bonus_pending, + R.string.gamification_APPCapps_button, + CardNotificationAction.DISCOVER, + referralModel.pendingAmount, + referralModel.symbol).takeIf { shouldShow } ?: EmptyNotification() + } + } + } + } + + override fun dismissNotification(referralNotification: ReferralNotification): Completable { + return defaultWallet.find() + .flatMapCompletable { + preferences.savePendingAmountNotification(it.address, + referralNotification.pendingAmount.scaleToString(2)) + } + } + + override fun getReferralInfo(): Single { + return promotionsRepository.getReferralInfo() + .map { mapResponse(it) } + } + + private fun isRedeemed(userStatus: ReferralResponse.UserStatus?): Boolean { + return userStatus?.let { it == ReferralResponse.UserStatus.REDEEMED } ?: false + } + + private fun isAvailable(status: PromotionsResponse.Status): Boolean { + return status == PromotionsResponse.Status.ACTIVE + } + + private fun mapResponse(referralResponse: ReferralResponse): ReferralModel { + return ReferralModel(referralResponse.completed, referralResponse.link, + referralResponse.invited, referralResponse.pendingAmount, referralResponse.amount, + referralResponse.symbol, referralResponse.maxAmount, referralResponse.minAmount, + referralResponse.available, referralResponse.receivedAmount, + isRedeemed(referralResponse.userStatus), isAvailable(referralResponse.status)) + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/ReferralInteractorContract.kt b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralInteractorContract.kt new file mode 100644 index 00000000000..5317094084a --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralInteractorContract.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.referrals + +import com.appcoins.wallet.gamification.repository.entity.ReferralResponse +import io.reactivex.Completable +import io.reactivex.Maybe +import io.reactivex.Single + +interface ReferralInteractorContract { + + fun hasReferralUpdate(walletAddress: String, + referralResponse: ReferralResponse?, + screen: ReferralsScreen): Single + + fun retrieveReferral(): Single + + fun saveReferralInformation(numberOfFriends: Int, isVerified: Boolean, + screen: ReferralsScreen): Completable + + fun getPendingBonusNotification(): Maybe + + fun getReferralInfo(): Single + + fun getUnwatchedPendingBonusNotification(): Single + + fun dismissNotification(referralNotification: ReferralNotification): Completable +} diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/ReferralLocalData.kt b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralLocalData.kt new file mode 100644 index 00000000000..b2499f070a6 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralLocalData.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.referrals + +import io.reactivex.Completable +import io.reactivex.Single + +interface ReferralLocalData { + fun saveReferralInformation(address: String, invitedFriends: Int, + isVerified: Boolean, screen: String): Completable + + fun getReferralInformation(address: String, screen: String): Single + + fun savePendingAmountNotification(address: String, pendingAmount: String): Completable + + fun getPendingAmountNotification(address: String): Single +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/ReferralModel.kt b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralModel.kt new file mode 100644 index 00000000000..1dd1f4d99ca --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralModel.kt @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.referrals + +import java.math.BigDecimal + +data class ReferralModel( + val completed: Int, val link: String?, val invited: Boolean, val pendingAmount: BigDecimal, + val amount: BigDecimal, val symbol: String, val maxAmount: BigDecimal, + val minAmount: BigDecimal, val available: Int, val receivedAmount: BigDecimal, + val isRedeemed: Boolean, val isActive: Boolean) { + + constructor() : this(0, "", false, BigDecimal.ZERO, BigDecimal.ZERO, + "", BigDecimal.ZERO, BigDecimal.ZERO, 0, BigDecimal.ZERO, false, + false) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/ReferralNotification.kt b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralNotification.kt new file mode 100644 index 00000000000..b91470f8fc8 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralNotification.kt @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.referrals + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction +import java.math.BigDecimal + +data class ReferralNotification(@StringRes override val title: Int, + @StringRes override val body: Int, + @DrawableRes override val icon: Int, @StringRes + override val positiveButtonText: Int, + override val positiveAction: CardNotificationAction, + val pendingAmount: BigDecimal, val symbol: String) : + CardNotification(title, body, icon, positiveButtonText, positiveAction) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/ReferralsFragment.kt b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralsFragment.kt new file mode 100644 index 00000000000..cb091ae3a33 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralsFragment.kt @@ -0,0 +1,175 @@ +package com.asfoundation.wallet.referrals + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.invited_friends_animation_list.* +import kotlinx.android.synthetic.main.referrals_layout.* +import java.math.BigDecimal +import javax.inject.Inject +import kotlin.math.roundToInt + +class ReferralsFragment : DaggerFragment(), ReferralsView { + + private lateinit var presenter: ReferralsPresenter + + @Inject + lateinit var formatter: CurrencyFormatUtils + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = ReferralsPresenter(this, CompositeDisposable()) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.referrals_layout, container, false) + } + + override fun setupLayout() { + val totalAvailable = completedInvites + available + friends_invited.text = String.format("%d/%d", completedInvites, totalAvailable) + friends_invited.visibility = VISIBLE + number_friends_invited.text = String.format("%d/%d", completedInvites, totalAvailable) + total_earned.text = currency.plus(formatter.formatCurrency(getTotalEarned(), + WalletCurrency.FIAT)) + total_earned.visibility = VISIBLE + val individualEarn = currency.plus(formatter.formatCurrency(amount, WalletCurrency.FIAT)) + val totalEarn = + currency.plus(formatter.formatCurrency(amount.multiply(BigDecimal(totalAvailable)), + WalletCurrency.FIAT)) + referral_explanation.text = + getString(R.string.referral_dropup_menu_requirements_body, individualEarn, totalEarn) + invitations_progress_bar.progress = + ((100 / (completedInvites.toDouble() + available.toDouble())) * completedInvites).roundToInt() + setFriendsAnimations(completedInvites, completedInvites + available) + } + + override fun bottomSheetHeaderClick() = RxView.clicks(bottom_sheet_header) + + override fun changeBottomSheetState() { + val parentFragment = provideParentFragment() + parentFragment?.changeBottomSheetState() + } + + private fun provideParentFragment(): InviteFriendsFragment? { + if (parentFragment !is InviteFriendsFragment) { + return null + } + return parentFragment as InviteFriendsFragment + } + + private fun getTotalEarned(): BigDecimal { + return if (!isRedeemed) receivedAmount else { + amount.multiply(BigDecimal(completedInvites)) + } + } + + private fun setFriendsAnimations(invited: Int, totalInvitations: Int) { + val friendsAnimation = + arrayOf(friend_animation_1, friend_animation_2, friend_animation_3, friend_animation_4, + friend_animation_5) + + for (animationIndex in friendsAnimation.indices) { + if (animationIndex < invited) { + friendsAnimation[animationIndex].setAnimation(R.raw.invited_user_animation) + friendsAnimation[animationIndex].playAnimation() + } + if (animationIndex >= totalInvitations) { + //If there are less animation icons than total invitations, remove extra icons + friendsAnimation[animationIndex].visibility = GONE + } + } + } + + private val receivedAmount: BigDecimal by lazy { + if (arguments!!.containsKey(RECEIVED_AMOUNT)) { + arguments!!.getSerializable(RECEIVED_AMOUNT) as BigDecimal + } else { + throw IllegalArgumentException("Received amount not found") + } + } + + private val amount: BigDecimal by lazy { + if (arguments!!.containsKey(AMOUNT)) { + arguments!!.getSerializable(AMOUNT) as BigDecimal + } else { + throw IllegalArgumentException("Amount not found") + } + } + + private val currency: String by lazy { + if (arguments!!.containsKey(CURRENCY)) { + arguments!!.getString(CURRENCY, "") + } else { + throw IllegalArgumentException("Currency not found") + } + } + + private val completedInvites: Int by lazy { + if (arguments!!.containsKey(COMPLETED_INVITES)) { + arguments!!.getInt(COMPLETED_INVITES) + } else { + throw IllegalArgumentException("Completed not found") + } + } + + private val available: Int by lazy { + if (arguments!!.containsKey(AVAILABLE)) { + arguments!!.getInt(AVAILABLE) + } else { + throw IllegalArgumentException("available not found") + } + } + + private val isRedeemed: Boolean by lazy { + if (arguments!!.containsKey(IS_REDEEMED)) { + arguments!!.getBoolean(IS_REDEEMED) + } else { + throw IllegalArgumentException("is redeemed not found") + } + } + + companion object { + + private const val AMOUNT = "amount" + private const val PENDING_AMOUNT = "pending_amount" + private const val COMPLETED_INVITES = "completed_invites" + private const val RECEIVED_AMOUNT = "received_amount" + private const val MAX_AMOUNT = "max_amount" + private const val AVAILABLE = "available" + private const val CURRENCY = "currency" + private const val IS_REDEEMED = "is_redeemed" + + fun newInstance(amount: BigDecimal, pendingAmount: BigDecimal, currency: String, + completed: Int, receivedAmount: BigDecimal, maxAmount: BigDecimal, + available: Int, isRedeemed: Boolean): ReferralsFragment { + val bundle = Bundle() + bundle.putSerializable(AMOUNT, amount) + bundle.putSerializable(PENDING_AMOUNT, pendingAmount) + bundle.putString(CURRENCY, currency) + bundle.putInt(COMPLETED_INVITES, completed) + bundle.putSerializable(RECEIVED_AMOUNT, receivedAmount) + bundle.putSerializable(MAX_AMOUNT, maxAmount) + bundle.putInt(AVAILABLE, available) + bundle.putBoolean(IS_REDEEMED, isRedeemed) + val fragment = ReferralsFragment() + fragment.arguments = bundle + return fragment + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/ReferralsPresenter.kt b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralsPresenter.kt new file mode 100644 index 00000000000..194c7161dd0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralsPresenter.kt @@ -0,0 +1,19 @@ +package com.asfoundation.wallet.referrals + +import io.reactivex.disposables.CompositeDisposable + +class ReferralsPresenter(private val view: ReferralsView, + private val disposables: CompositeDisposable) { + + fun present() { + view.setupLayout() + handleBottomSheetHeaderClick() + } + + private fun handleBottomSheetHeaderClick() { + disposables.add(view.bottomSheetHeaderClick() + .doOnNext { view.changeBottomSheetState() } + .subscribe({}, { it.printStackTrace() })) + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/ReferralsScreen.kt b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralsScreen.kt new file mode 100644 index 00000000000..de1160c61cb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralsScreen.kt @@ -0,0 +1,5 @@ +package com.asfoundation.wallet.referrals + +enum class ReferralsScreen { + PROMOTIONS, INVITE_FRIENDS +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/ReferralsView.kt b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralsView.kt new file mode 100644 index 00000000000..1f61de5e559 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/ReferralsView.kt @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.referrals + +import io.reactivex.Observable + +interface ReferralsView { + fun setupLayout() + fun bottomSheetHeaderClick(): Observable + fun changeBottomSheetState() +} diff --git a/app/src/main/java/com/asfoundation/wallet/referrals/SharedPreferencesReferralLocalData.kt b/app/src/main/java/com/asfoundation/wallet/referrals/SharedPreferencesReferralLocalData.kt new file mode 100644 index 00000000000..92d18f894a3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/referrals/SharedPreferencesReferralLocalData.kt @@ -0,0 +1,53 @@ +package com.asfoundation.wallet.referrals + +import android.content.SharedPreferences +import io.reactivex.Completable +import io.reactivex.Single + +class SharedPreferencesReferralLocalData(private val preferences: SharedPreferences) : + ReferralLocalData { + + override fun saveReferralInformation(address: String, invitedFriends: Int, + isVerified: Boolean, screen: String): Completable { + return Completable.fromCallable { + preferences.edit() + .putString(getKey(address, screen), invitedFriends.toString() + isVerified) + .apply() + } + } + + override fun getReferralInformation(address: String, screen: String): Single { + return Single.fromCallable { + preferences.getString(getKey(address, screen), "-1") + } + } + + override fun savePendingAmountNotification(address: String, pendingAmount: String): Completable { + return Completable.fromCallable { + preferences.edit() + .putString(getKey(address), pendingAmount) + .apply() + } + } + + override fun getPendingAmountNotification(address: String): Single { + return Single.fromCallable { + preferences.getString(getKey(address), "0") + } + } + + private fun getKey(wallet: String, screen: String): String { + return CONTEXT + wallet + SCREEN + screen + } + + private fun getKey(wallet: String): String { + return CONTEXT + wallet + PENDING_AMOUNT + } + + companion object { + private const val CONTEXT = "REFERRALS" + private const val SCREEN = "screen_" + private const val PENDING_AMOUNT = "referrals_pending_amount" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/AllowanceService.kt b/app/src/main/java/com/asfoundation/wallet/repository/AllowanceService.kt new file mode 100644 index 00000000000..9e08440ac83 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/AllowanceService.kt @@ -0,0 +1,70 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.entity.TokenInfo +import com.asfoundation.wallet.interact.DefaultTokenProvider +import io.reactivex.Single +import org.web3j.abi.FunctionEncoder +import org.web3j.abi.FunctionReturnDecoder +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.Address +import org.web3j.abi.datatypes.Function +import org.web3j.abi.datatypes.generated.Uint256 +import org.web3j.protocol.Web3j +import org.web3j.protocol.core.DefaultBlockParameterName +import org.web3j.protocol.core.methods.request.Transaction +import java.math.BigDecimal +import java.math.BigInteger + +class AllowanceService(private val web3j: Web3j, + private val defaultTokenProvider: DefaultTokenProvider) { + + fun checkAllowance(owner: String, spender: String, + tokenAddress: String): Single { + return defaultTokenProvider.defaultToken + .map { tokenInfo: TokenInfo -> + + val function = + allowance(owner, spender) + val responseValue = + callSmartContractFunction(function, tokenAddress, owner) + val response = + FunctionReturnDecoder.decode(responseValue, + function.outputParameters) + + if (response.size == 1) { + return@map BigDecimal( + (response[0] as Uint256).value) + .multiply( + BigDecimal(BigInteger.ONE, + tokenInfo.decimals)) + } else { + throw IllegalStateException("Failed to execute contract call!") + } + + } + } + + @Throws(Exception::class) + private fun callSmartContractFunction(function: Function, + contractAddress: String, + walletAddress: String): String { + val encodedFunction = FunctionEncoder.encode(function) + val transaction = + Transaction.createEthCallTransaction(walletAddress, + contractAddress, encodedFunction) + return web3j.ethCall(transaction, DefaultBlockParameterName.LATEST) + .send() + .value + } + + companion object { + private fun allowance(owner: String, + spender: String): Function { + return Function("allowance", + listOf(Address(owner), + Address(spender)), + listOf(object : TypeReference() {})) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/ApproveService.java b/app/src/main/java/com/asfoundation/wallet/repository/ApproveService.java index 6cfba7d97a4..2cd488ed651 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/ApproveService.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/ApproveService.java @@ -1,9 +1,8 @@ package com.asfoundation.wallet.repository; -import com.asfoundation.wallet.interact.SendTransactionInteract; +import com.asfoundation.wallet.entity.TransactionBuilder; import io.reactivex.Completable; import io.reactivex.Observable; -import io.reactivex.Scheduler; import java.util.List; /** @@ -11,65 +10,119 @@ */ public class ApproveService { - private final SendTransactionInteract sendTransactionInteract; - private final Repository cache; - private final ErrorMapper errorMapper; - private final Scheduler scheduler; - - public ApproveService(SendTransactionInteract sendTransactionInteract, - Repository cache, ErrorMapper errorMapper, Scheduler scheduler) { - this.sendTransactionInteract = sendTransactionInteract; - this.cache = cache; - this.errorMapper = errorMapper; - this.scheduler = scheduler; + private final WatchedTransactionService transactionService; + private final TransactionValidator approveTransactionSender; + + public ApproveService(WatchedTransactionService transactionService, + TransactionValidator approveTransactionSender) { + this.transactionService = transactionService; + this.approveTransactionSender = approveTransactionSender; } public void start() { - cache.getAll() - .observeOn(scheduler) - .flatMapCompletable(paymentTransactions -> Observable.fromIterable(paymentTransactions) - .filter(paymentTransaction -> paymentTransaction.getState() - .equals(PaymentTransaction.PaymentState.PENDING)) - .flatMapCompletable(this::approveTransaction)) - .doOnError(Throwable::printStackTrace) - .retry() - .subscribe(); + transactionService.start(); } - private Completable approveTransaction(PaymentTransaction paymentTransaction) { - return cache.save(paymentTransaction.getUri(), - new PaymentTransaction(paymentTransaction, PaymentTransaction.PaymentState.APPROVING)) - .observeOn(scheduler) - .andThen(sendTransactionInteract.approve(paymentTransaction.getTransactionBuilder(), - paymentTransaction.getNonce()) - .flatMapCompletable(hash -> saveTransaction(hash, paymentTransaction))) - .onErrorResumeNext(throwable -> cache.save(paymentTransaction.getUri(), - new PaymentTransaction(paymentTransaction, errorMapper.map(throwable)))); + public Completable approveWithoutValidation(String key, TransactionBuilder transactionBuilder) { + return transactionService.sendTransaction(key, transactionBuilder); } - private Completable saveTransaction(String hash, PaymentTransaction paymentTransaction) { - return cache.save(paymentTransaction.getUri(), - new PaymentTransaction(paymentTransaction, PaymentTransaction.PaymentState.APPROVED, hash)); + public Completable approve(String key, PaymentTransaction paymentTransaction) { + return approveTransactionSender.validate(paymentTransaction) + .andThen( + transactionService.sendTransaction(key, paymentTransaction.getTransactionBuilder())); } - public Completable approve(String key, PaymentTransaction paymentTransaction) { - return cache.save(key, - new PaymentTransaction(paymentTransaction, PaymentTransaction.PaymentState.PENDING)); + public Observable getApprove(String uri) { + return transactionService.getTransaction(uri) + .map(this::map); } - public Observable getApprove(String uri) { - return cache.get(uri); + private ApproveTransaction map(Transaction transaction) { + return new ApproveTransaction(transaction.getKey(), + mapTransactionState(transaction.getStatus()), transaction.getTransactionHash()); } - public Observable> getAll() { - return cache.getAll() - .flatMapSingle(paymentTransactions -> Observable.fromIterable(paymentTransactions) - .filter(paymentTransaction -> !paymentTransaction.getState() - .equals(PaymentTransaction.PaymentState.PENDING)) + private Status mapTransactionState(Transaction.Status status) { + Status toReturn; + switch (status) { + case PENDING: + toReturn = Status.PENDING; + break; + case PROCESSING: + toReturn = Status.APPROVING; + break; + case COMPLETED: + toReturn = Status.APPROVED; + break; + default: + case ERROR: + toReturn = Status.ERROR; + break; + case WRONG_NETWORK: + toReturn = Status.WRONG_NETWORK; + break; + case NONCE_ERROR: + toReturn = Status.NONCE_ERROR; + break; + case UNKNOWN_TOKEN: + toReturn = Status.UNKNOWN_TOKEN; + break; + case NO_TOKENS: + toReturn = Status.NO_TOKENS; + break; + case NO_ETHER: + toReturn = Status.NO_ETHER; + break; + case NO_FUNDS: + toReturn = Status.NO_FUNDS; + break; + case NO_INTERNET: + toReturn = Status.NO_INTERNET; + break; + case FORBIDDEN: + toReturn = Status.FORBIDDEN; + break; + } + return toReturn; + } + + public Observable> getAll() { + return transactionService.getAll() + .flatMapSingle(transactions -> Observable.fromIterable(transactions) + .map(this::map) .toList()); } public Completable remove(String key) { - return cache.remove(key); + return transactionService.remove(key); + } + + public enum Status { + PENDING, APPROVING, APPROVED, ERROR, WRONG_NETWORK, NONCE_ERROR, UNKNOWN_TOKEN, NO_TOKENS, NO_ETHER, NO_FUNDS, NO_INTERNET, FORBIDDEN + } + + public class ApproveTransaction { + private final String key; + private final Status status; + private final String transactionHash; + + public ApproveTransaction(String key, Status status, String transactionHash) { + this.key = key; + this.status = status; + this.transactionHash = transactionHash; + } + + public String getKey() { + return key; + } + + public Status getStatus() { + return status; + } + + public String getTransactionHash() { + return transactionHash; + } } } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/ApproveTransactionValidatorBds.java b/app/src/main/java/com/asfoundation/wallet/repository/ApproveTransactionValidatorBds.java new file mode 100644 index 00000000000..e23ec1c0fd2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/ApproveTransactionValidatorBds.java @@ -0,0 +1,52 @@ +package com.asfoundation.wallet.repository; + +import com.appcoins.wallet.bdsbilling.AuthorizationProof; +import com.appcoins.wallet.bdsbilling.BillingPaymentProofSubmission; +import com.asfoundation.wallet.billing.partners.AddressService; +import com.asfoundation.wallet.interact.SendTransactionInteract; +import io.reactivex.Completable; +import io.reactivex.Single; +import java.math.BigDecimal; + +public class ApproveTransactionValidatorBds implements TransactionValidator { + private final SendTransactionInteract sendTransactionInteract; + private final BillingPaymentProofSubmission billingPaymentProofSubmission; + private final AddressService partnerAddressService; + + public ApproveTransactionValidatorBds(SendTransactionInteract sendTransactionInteract, + BillingPaymentProofSubmission billingPaymentProofSubmission, + AddressService partnerAddressService) { + this.sendTransactionInteract = sendTransactionInteract; + this.billingPaymentProofSubmission = billingPaymentProofSubmission; + this.partnerAddressService = partnerAddressService; + } + + @Override public Completable validate(PaymentTransaction paymentTransaction) { + String packageName = paymentTransaction.getPackageName(); + String developerAddress = paymentTransaction.getTransactionBuilder() + .toAddress(); + String productName = paymentTransaction.getTransactionBuilder() + .getSkuId(); + String type = paymentTransaction.getTransactionBuilder() + .getType(); + BigDecimal priceValue = paymentTransaction.getTransactionBuilder() + .amount(); + Single getTransactionHash = sendTransactionInteract.computeApproveTransactionHash( + paymentTransaction.getTransactionBuilder()); + Single getStoreAddress = + partnerAddressService.getStoreAddressForPackage(paymentTransaction.getPackageName()); + Single getOemAddress = + partnerAddressService.getOemAddressForPackage(paymentTransaction.getPackageName()); + + return Single.zip(getTransactionHash, getStoreAddress, getOemAddress, + (hash, storeAddress, oemAddress) -> new AuthorizationProof("appcoins", hash, productName, + packageName, priceValue, storeAddress, oemAddress, developerAddress, type, + paymentTransaction.getTransactionBuilder() + .getOrigin() == null ? "BDS" : paymentTransaction.getTransactionBuilder() + .getOrigin(), paymentTransaction.getDeveloperPayload(), + paymentTransaction.getCallbackUrl(), paymentTransaction.getTransactionBuilder() + .getOrderReference(), paymentTransaction.getTransactionBuilder() + .getReferrerUrl())) + .flatMapCompletable(billingPaymentProofSubmission::processAuthorizationProof); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/AutoUpdateRepository.kt b/app/src/main/java/com/asfoundation/wallet/repository/AutoUpdateRepository.kt new file mode 100644 index 00000000000..ec625799e2c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/AutoUpdateRepository.kt @@ -0,0 +1,18 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.service.AutoUpdateService +import com.asfoundation.wallet.viewmodel.AutoUpdateModel +import io.reactivex.Single + +class AutoUpdateRepository(private val autoUpdateService: AutoUpdateService) { + + private var autoUpdateModel = AutoUpdateModel() + + fun loadAutoUpdateModel(invalidateCache: Boolean): Single { + if (autoUpdateModel.isValid() && !invalidateCache) { + return Single.just(autoUpdateModel) + } + return autoUpdateService.loadAutoUpdateModel() + .doOnSuccess { if (it.isValid()) autoUpdateModel = it } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/BackendTransactionRepository.kt b/app/src/main/java/com/asfoundation/wallet/repository/BackendTransactionRepository.kt new file mode 100644 index 00000000000..91847156136 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/BackendTransactionRepository.kt @@ -0,0 +1,126 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.entity.NetworkInfo +import com.asfoundation.wallet.interact.DefaultTokenProvider +import com.asfoundation.wallet.poa.BlockchainErrorMapper +import com.asfoundation.wallet.repository.entity.TransactionEntity +import com.asfoundation.wallet.service.AccountKeystoreService +import com.asfoundation.wallet.transactions.Transaction +import com.asfoundation.wallet.ui.iab.raiden.MultiWalletNonceObtainer +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import java.util.* +import java.util.concurrent.TimeUnit + +class BackendTransactionRepository( + networkInfo: NetworkInfo, + accountKeystoreService: AccountKeystoreService, + defaultTokenProvider: DefaultTokenProvider, + errorMapper: BlockchainErrorMapper, + nonceObtainer: MultiWalletNonceObtainer, + scheduler: Scheduler, + private val offChainTransactions: OffChainTransactions, + private val localRepository: TransactionsRepository, + private val mapper: TransactionMapper, + private val disposables: CompositeDisposable, + private val ioScheduler: Scheduler) : + TransactionRepository(networkInfo, accountKeystoreService, + defaultTokenProvider, errorMapper, nonceObtainer, scheduler) { + + private lateinit var disposable: Disposable + override fun fetchTransaction(wallet: String): Observable> { + if (!::disposable.isInitialized || disposable.isDisposed) { + disposable = getLastProcessedTime(wallet) + .subscribeOn(ioScheduler) + .flatMapObservable { startingDate -> + return@flatMapObservable Observable.merge( + fetchNewTransactions(wallet, startingDate = startingDate), + fetchMissingOldTransactions(wallet)) + } + .buffer(2, TimeUnit.SECONDS) + .doOnNext { localRepository.insertAll(it.flatten()) } + .subscribe({}, { it.printStackTrace() }) + } + disposables.add(disposable) + + return localRepository.getAllAsFlowable(wallet) + .map { mapper.map(it) } + .toObservable() + .distinctUntilChanged() + } + + override fun fetchNewTransactions(wallet: String): Single> { + return localRepository.getNewestTransaction(wallet) + .map { it.processedTime } + .defaultIfEmpty(0) + .subscribeOn(ioScheduler) + .flatMapSingle { startingDate -> + //We need +1 otherwise since the transaction on the backend is stored with 6 milliseconds + // and we store with 3, so the last transaction will always be returned + fetchNewTransactions(wallet, startingDate + 1).firstOrError() + } + .doOnSuccess { localRepository.insertAll(it) } + .map { mapper.map(it) } + } + + private fun fetchNewTransactions(wallet: String, + startingDate: Long): Observable> { + var sort = OffChainTransactions.Sort.DESC + if (startingDate != 0L) { + sort = OffChainTransactions.Sort.ASC + } + return fetchTransactions(wallet, startingDate = startingDate, sort = sort) + } + + private fun fetchMissingOldTransactions( + wallet: String): Observable> { + return localRepository.isOldTransactionsLoaded() + .flatMapObservable { isLoaded -> + if (isLoaded) { + return@flatMapObservable Observable.empty>() + } + return@flatMapObservable localRepository.getOlderTransaction(wallet) + .map { it.processedTime } + .flatMapObservable { + fetchTransactions(wallet, 0L, it, OffChainTransactions.Sort.DESC) + } + .doOnComplete { localRepository.oldTransactionsLoaded() } + } + + } + + private fun fetchTransactions(wallet: String, + startingDate: Long? = null, + endDate: Long? = null, + sort: OffChainTransactions.Sort? = null): Observable> { + return TransactionsLoadObservable(offChainTransactions, wallet, startingDate, endDate, sort) + .flatMapSingle { transactions -> + Observable.fromIterable(transactions) + .map { mapper.map(it, wallet) } + .toList() + } + } + + private fun getLastProcessedTime(wallet: String): Maybe { + val lastLocale = localRepository.getLastLocale() + val currentLocale = Locale.getDefault().language + return if (lastLocale == null || lastLocale == currentLocale) { + if (lastLocale == null) localRepository.setLocale(currentLocale) + localRepository.getNewestTransaction(wallet) + .map { it.processedTime } + .defaultIfEmpty(0) + } else { + Maybe.fromCallable { + localRepository.setLocale(currentLocale) + localRepository.deleteAllTransactions() + 0L + } + } + } + + override fun stop() = disposables.clear() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/BalanceService.java b/app/src/main/java/com/asfoundation/wallet/repository/BalanceService.java index 5ec3021589c..0502d5b013f 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/BalanceService.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/BalanceService.java @@ -1,11 +1,11 @@ package com.asfoundation.wallet.repository; import com.asfoundation.wallet.entity.TransactionBuilder; -import com.asfoundation.wallet.interact.GetDefaultWalletBalance; +import com.asfoundation.wallet.interact.GetDefaultWalletBalanceInteract; import io.reactivex.Single; import java.math.BigDecimal; public interface BalanceService { - Single hasEnoughBalance( + Single hasEnoughBalance( TransactionBuilder transactionBuilder, BigDecimal transactionGasLimit); } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/BdsBackEndWriter.kt b/app/src/main/java/com/asfoundation/wallet/repository/BdsBackEndWriter.kt new file mode 100644 index 00000000000..77510bf93c1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/BdsBackEndWriter.kt @@ -0,0 +1,16 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.poa.Proof +import com.asfoundation.wallet.poa.ProofWriter +import com.asfoundation.wallet.service.CampaignService +import io.reactivex.Single + +open class BdsBackEndWriter(private val defaultWalletInteract: FindDefaultWalletInteract, + private val service: CampaignService) : ProofWriter { + + override fun writeProof(proof: Proof): Single { + return defaultWalletInteract.find() + .flatMap { wallet -> service.submitProof(proof, wallet.address) } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/BdsPendingTransactionService.java b/app/src/main/java/com/asfoundation/wallet/repository/BdsPendingTransactionService.java new file mode 100644 index 00000000000..db4e734e0ca --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/BdsPendingTransactionService.java @@ -0,0 +1,39 @@ +package com.asfoundation.wallet.repository; + +import com.appcoins.wallet.bdsbilling.Billing; +import com.appcoins.wallet.bdsbilling.BillingPaymentProofSubmission; +import com.appcoins.wallet.bdsbilling.repository.entity.Transaction; +import com.asfoundation.wallet.entity.PendingTransaction; +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import java.util.concurrent.TimeUnit; + +public class BdsPendingTransactionService implements TrackTransactionService { + private final Billing billing; + private final Scheduler scheduler; + private final long period; + private final BillingPaymentProofSubmission billingPaymentProofSubmission; + + public BdsPendingTransactionService(Billing billing, Scheduler scheduler, long period, + BillingPaymentProofSubmission billingPaymentProofSubmission) { + this.billing = billing; + this.scheduler = scheduler; + this.period = period; + this.billingPaymentProofSubmission = billingPaymentProofSubmission; + } + + @Override public Observable checkTransactionState(String hash) { + return checkTransactionStateFromTransactionId( + billingPaymentProofSubmission.getTransactionId(hash)); + } + + public Observable checkTransactionStateFromTransactionId(String uid) { + return Observable.interval(period, TimeUnit.SECONDS, scheduler) + .timeInterval() + .switchMap(scan -> billing.getAppcoinsTransaction(uid, scheduler) + .map(transaction -> new PendingTransaction(transaction.getUid(), + transaction.getStatus() == Transaction.Status.PROCESSING)) + .toObservable()) + .takeUntil(pendingTransaction -> !pendingTransaction.isPending()); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/BdsTransactionProvider.java b/app/src/main/java/com/asfoundation/wallet/repository/BdsTransactionProvider.java new file mode 100644 index 00000000000..35cabfc076b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/BdsTransactionProvider.java @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.repository; + +import com.appcoins.wallet.bdsbilling.repository.entity.Transaction; +import io.reactivex.Single; + +public interface BdsTransactionProvider { + Single get(String packageName, String sku); +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/BdsTransactionService.java b/app/src/main/java/com/asfoundation/wallet/repository/BdsTransactionService.java new file mode 100644 index 00000000000..160119f7b83 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/BdsTransactionService.java @@ -0,0 +1,152 @@ +package com.asfoundation.wallet.repository; + +import com.appcoins.wallet.commons.Repository; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import io.reactivex.disposables.CompositeDisposable; + +public class BdsTransactionService { + private final Scheduler scheduler; + private final Repository cache; + private final CompositeDisposable disposables; + private final BdsPendingTransactionService transactionService; + + public BdsTransactionService(Scheduler scheduler, Repository cache, + CompositeDisposable disposables, BdsPendingTransactionService transactionService) { + this.scheduler = scheduler; + this.cache = cache; + this.disposables = disposables; + this.transactionService = transactionService; + } + + public Observable getTransaction(String key) { + return cache.get(key) + .filter(transaction -> !transaction.getStatus() + .equals(BdsTransaction.Status.WAITING)); + } + + public void start() { + disposables.add(cache.getAll() + .subscribeOn(scheduler) + .flatMapIterable(bdsTransactions -> bdsTransactions) + .filter(bdsTransaction -> bdsTransaction.getStatus() + .equals(BdsTransaction.Status.WAITING)) + .flatMapCompletable(bdsTransaction -> cache.save(bdsTransaction.getKey(), + new BdsTransaction(bdsTransaction, BdsTransaction.Status.PROCESSING)) + .andThen( + transactionService.checkTransactionStateFromTransactionId(bdsTransaction.getUid()) + .flatMapCompletable(pendingTransaction -> cache.save(bdsTransaction.getKey(), + new BdsTransaction(bdsTransaction, + pendingTransaction.isPending() ? BdsTransaction.Status.PROCESSING + : BdsTransaction.Status.COMPLETED))))) + .doOnError(Throwable::printStackTrace) + .retry() + .subscribe()); + } + + public void stop() { + disposables.clear(); + } + + public Completable trackTransaction(String key, String packageName, String skuId, String uid, + String orderReference) { + return cache.save(key, new BdsTransaction(uid, key, packageName, skuId, orderReference)); + } + + public Completable remove(String uri) { + return cache.remove(uri); + } + + public static class BdsTransaction { + private final String key; + private final String skuId; + private final String packageName; + private final Status status; + private final String uid; + private final String orderReference; + + public BdsTransaction(String uid, String key, String packageName, String skuId, + String orderReference) { + this.uid = uid; + this.key = key; + this.packageName = packageName; + this.skuId = skuId; + this.orderReference = orderReference; + this.status = Status.WAITING; + } + + public BdsTransaction(BdsTransaction transaction, Status status) { + this.key = transaction.getKey(); + this.skuId = transaction.getSkuId(); + this.packageName = transaction.getPackageName(); + this.uid = transaction.getUid(); + this.status = status; + this.orderReference = transaction.orderReference; + } + + @Override public int hashCode() { + int result = key.hashCode(); + result = 31 * result + skuId.hashCode(); + result = 31 * result + packageName.hashCode(); + result = 31 * result + status.hashCode(); + return result; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof BdsTransaction)) return false; + + BdsTransaction that = (BdsTransaction) o; + + if (!key.equals(that.key)) return false; + if (!skuId.equals(that.skuId)) return false; + if (!packageName.equals(that.packageName)) return false; + return status == that.status; + } + + @Override public String toString() { + return "BdsTransaction{" + + "key='" + + key + + '\'' + + ", skuId='" + + skuId + + '\'' + + ", packageName='" + + packageName + + '\'' + + ", status=" + + status + + '}'; + } + + public String getKey() { + return key; + } + + public String getPackageName() { + return packageName; + } + + public String getOrderReference() { + return orderReference; + } + + public String getSkuId() { + return skuId; + } + + public Status getStatus() { + return status; + } + + public String getUid() { + return uid; + } + + public enum Status { + WAITING, PROCESSING, UNKNOWN_STATUS, COMPLETED + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/BlockChainWriter.java b/app/src/main/java/com/asfoundation/wallet/repository/BlockChainWriter.java deleted file mode 100644 index 8b5d50414a9..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/BlockChainWriter.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.asfoundation.wallet.repository; - -import android.util.Log; -import com.asfoundation.wallet.entity.GasSettings; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.poa.Proof; -import com.asfoundation.wallet.poa.ProofSubmissionFeeData; -import com.asfoundation.wallet.poa.ProofWriter; -import com.asfoundation.wallet.poa.TransactionFactory; -import io.reactivex.Single; -import java.math.BigDecimal; -import java.net.UnknownHostException; -import org.web3j.protocol.core.methods.response.EthSendTransaction; -import org.web3j.utils.Numeric; - -public class BlockChainWriter implements ProofWriter { - private final Web3jProvider web3jProvider; - private final TransactionFactory transactionFactory; - private final WalletRepositoryType walletRepositoryType; - private final FindDefaultWalletInteract defaultWalletInteract; - private final GasSettingsRepositoryType gasSettingsRepository; - private final BigDecimal registerPoaGasLimit; - private final EthereumNetworkRepositoryType ethereumNetwork; - - public BlockChainWriter(Web3jProvider web3jProvider, TransactionFactory transactionFactory, - WalletRepositoryType walletRepositoryType, FindDefaultWalletInteract defaultWalletInteract, - GasSettingsRepositoryType gasSettingsRepository, BigDecimal registerPoaGasLimit, - EthereumNetworkRepositoryType ethereumNetwork) { - this.web3jProvider = web3jProvider; - this.transactionFactory = transactionFactory; - this.walletRepositoryType = walletRepositoryType; - this.defaultWalletInteract = defaultWalletInteract; - this.gasSettingsRepository = gasSettingsRepository; - this.registerPoaGasLimit = registerPoaGasLimit; - this.ethereumNetwork = ethereumNetwork; - } - - @Override public Single writeProof(Proof proof) { - return transactionFactory.createTransaction(proof) - .flatMap(this::sendTransaction); - } - - @Override public Single hasEnoughFunds(int chainId) { - return ethereumNetwork.executeOnNetworkAndRestore(chainId, defaultWalletInteract.find() - .flatMap(walletRepositoryType::balanceInWei) - .flatMap(balance -> gasSettingsRepository.getGasSettings(true) - .map(gasSettings -> getFeeData( - balance.compareTo(registerPoaGasLimit.multiply(gasSettings.gasPrice)) >= 1, - gasSettings)))) - .onErrorResumeNext(throwable -> { - if (throwable instanceof WalletNotFoundException) { - return Single.just( - new ProofSubmissionFeeData(ProofSubmissionFeeData.RequirementsStatus.NO_WALLET, - BigDecimal.ZERO, BigDecimal.ZERO)); - } else if (throwable instanceof UnknownHostException) { - return Single.just( - new ProofSubmissionFeeData(ProofSubmissionFeeData.RequirementsStatus.NO_NETWORK, - BigDecimal.ZERO, BigDecimal.ZERO)); - } - return Single.error(throwable); - }); - } - - private ProofSubmissionFeeData getFeeData(boolean hasFunds, GasSettings gasSettings) { - if (hasFunds) { - return new ProofSubmissionFeeData(ProofSubmissionFeeData.RequirementsStatus.READY, - registerPoaGasLimit, gasSettings.gasPrice); - } - return new ProofSubmissionFeeData(ProofSubmissionFeeData.RequirementsStatus.NO_FUNDS, - BigDecimal.ZERO, BigDecimal.ZERO); - } - - private Single sendTransaction(byte[] transaction) { - return Single.fromCallable(() -> { - EthSendTransaction sentTransaction = web3jProvider.getDefault() - .ethSendRawTransaction(Numeric.toHexString(transaction)) - .send(); - if (sentTransaction.hasError()) { - throw new TransactionException(sentTransaction.getError() - .getCode(), sentTransaction.getError() - .getMessage(), sentTransaction.getError() - .getData()); - } - return sentTransaction.getTransactionHash(); - }); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/BuyService.java b/app/src/main/java/com/asfoundation/wallet/repository/BuyService.java index 6172ba1264e..fee18ce6df8 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/BuyService.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/BuyService.java @@ -1,105 +1,178 @@ package com.asfoundation.wallet.repository; -import com.asfoundation.wallet.entity.PendingTransaction; -import com.asfoundation.wallet.interact.SendTransactionInteract; +import androidx.annotation.NonNull; +import com.asfoundation.wallet.billing.partners.AddressService; +import com.asfoundation.wallet.entity.TokenInfo; +import com.asfoundation.wallet.entity.TransactionBuilder; +import com.asfoundation.wallet.interact.DefaultTokenProvider; +import com.asfoundation.wallet.poa.CountryCodeProvider; +import com.asfoundation.wallet.poa.DataMapper; import io.reactivex.Completable; -import io.reactivex.CompletableSource; import io.reactivex.Observable; -import io.reactivex.Scheduler; -import io.reactivex.schedulers.Schedulers; -import java.math.BigInteger; +import io.reactivex.Single; +import java.math.BigDecimal; import java.util.List; -import java.util.concurrent.TimeUnit; /** * Created by trinkes on 3/16/18. */ public class BuyService { - private final SendTransactionInteract sendTransactionInteract; - private final PendingTransactionService pendingTransactionService; - private final Repository cache; - private final ErrorMapper errorMapper; - private final Scheduler scheduler; - - public BuyService(SendTransactionInteract sendTransactionInteract, - PendingTransactionService pendingTransactionService, - Repository cache, - ErrorMapper errorMapper, Scheduler scheduler) { - this.sendTransactionInteract = sendTransactionInteract; - this.pendingTransactionService = pendingTransactionService; - this.cache = cache; - this.errorMapper = errorMapper; - this.scheduler = scheduler; + private final WatchedTransactionService transactionService; + private final TransactionValidator transactionValidator; + private final DefaultTokenProvider defaultTokenProvider; + private final CountryCodeProvider countryCodeProvider; + private final DataMapper dataMapper; + private final AddressService partnerAddressService; + + public BuyService(WatchedTransactionService transactionService, + TransactionValidator transactionValidator, DefaultTokenProvider defaultTokenProvider, + CountryCodeProvider countryCodeProvider, DataMapper dataMapper, + AddressService partnerAddressService) { + this.transactionService = transactionService; + this.transactionValidator = transactionValidator; + this.defaultTokenProvider = defaultTokenProvider; + this.countryCodeProvider = countryCodeProvider; + this.dataMapper = dataMapper; + this.partnerAddressService = partnerAddressService; } public void start() { - cache.getAll() - .observeOn(scheduler) - .flatMapCompletable(paymentTransactions -> Observable.fromIterable(paymentTransactions) - .filter(paymentTransaction -> paymentTransaction.getState() - .equals(PaymentTransaction.PaymentState.APPROVED)) - .flatMapCompletable(paymentTransaction -> cache.save(paymentTransaction.getUri(), - new PaymentTransaction(paymentTransaction, PaymentTransaction.PaymentState.BUYING)) - .andThen(buy(paymentTransaction)))) - .doOnError(Throwable::printStackTrace) - .retry() - .subscribe(); + transactionService.start(); } - private Completable buy(PaymentTransaction paymentTransaction) { - return sendTransactionInteract.buy(paymentTransaction.getTransactionBuilder(), - paymentTransaction.getNonce() - .add(BigInteger.ONE)) - .flatMapCompletable(hash -> pendingTransactionService.checkTransactionState(hash) - .retryWhen(this::retryOnTransactionNotFound) - .firstOrError() - .flatMapCompletable(pendingTransaction -> saveTransaction(pendingTransaction, - paymentTransaction).onErrorResumeNext( - throwable -> saveError(paymentTransaction, throwable)))) - .onErrorResumeNext(throwable -> saveError(paymentTransaction, throwable)); + public Completable buy(String key, PaymentTransaction paymentTransaction) { + TransactionBuilder transactionBuilder = paymentTransaction.getTransactionBuilder(); + return Single.zip(countryCodeProvider.getCountryCode(), defaultTokenProvider.getDefaultToken(), + partnerAddressService.getStoreAddressForPackage(paymentTransaction.getPackageName()), + partnerAddressService.getOemAddressForPackage(paymentTransaction.getPackageName()), + (countryCode, tokenInfo, storeAddress, oemAddress) -> transactionBuilder.appcoinsData( + getBuyData(transactionBuilder, tokenInfo, paymentTransaction.getPackageName(), + countryCode, storeAddress, oemAddress))) + .map(transaction -> updateTransactionBuilderData(paymentTransaction, transaction)) + .flatMapCompletable( + payment -> Completable.defer(() -> transactionValidator.validate(payment)) + .andThen(transactionService.sendTransaction(key, payment.getTransactionBuilder()))); } - private Observable retryOnTransactionNotFound(Observable throwableObservable) { - return throwableObservable.flatMap(throwable -> { - if (throwable instanceof TransactionNotFoundException) { - return Observable.timer(1, TimeUnit.SECONDS, Schedulers.trampoline()); - } - return Observable.error(throwable); - }); + public Observable getBuy(String uri) { + return transactionService.getTransaction(uri) + .map(this::mapTransaction); } - private CompletableSource saveError(PaymentTransaction paymentTransaction, Throwable throwable) { - throwable.printStackTrace(); - return cache.save(paymentTransaction.getUri(), - new PaymentTransaction(paymentTransaction, errorMapper.map(throwable))); + public Observable> getAll() { + return transactionService.getAll() + .flatMapSingle(entries -> Observable.fromIterable(entries) + .map(this::mapTransaction) + .toList()); } - private Completable saveTransaction(PendingTransaction pendingTransaction, - PaymentTransaction paymentTransaction) { - return cache.save(paymentTransaction.getUri(), - new PaymentTransaction(paymentTransaction, PaymentTransaction.PaymentState.BOUGHT, - paymentTransaction.getApproveHash(), pendingTransaction.getHash())); + public Completable remove(String key) { + return transactionService.remove(key); } - public Completable buy(String key, PaymentTransaction paymentTransaction) { - return cache.save(key, - new PaymentTransaction(paymentTransaction, PaymentTransaction.PaymentState.APPROVED)); + @NonNull + private PaymentTransaction updateTransactionBuilderData(PaymentTransaction paymentTransaction, + TransactionBuilder transaction) { + return new PaymentTransaction(paymentTransaction.getUri(), transaction, + paymentTransaction.getState(), paymentTransaction.getApproveHash(), + paymentTransaction.getBuyHash(), paymentTransaction.getPackageName(), + paymentTransaction.getProductName(), paymentTransaction.getProductId(), + paymentTransaction.getDeveloperPayload(), paymentTransaction.getCallbackUrl(), + paymentTransaction.getOrderReference(), paymentTransaction.getErrorCode(), + paymentTransaction.getErrorMessage()); } - public Observable getBuy(String uri) { - return cache.get(uri); + private byte[] getBuyData(TransactionBuilder transactionBuilder, TokenInfo tokenInfo, + String packageName, String countryCode, String storeAddress, String oemAddress) { + return TokenRepository.buyData(transactionBuilder.toAddress(), storeAddress, oemAddress, + transactionBuilder.getSkuId(), transactionBuilder.amount() + .multiply(new BigDecimal("10").pow(transactionBuilder.decimals())), tokenInfo.address, + packageName, dataMapper.convertCountryCode(countryCode)); } - public Observable> getAll() { - return cache.getAll() - .flatMapSingle(paymentTransactions -> Observable.fromIterable(paymentTransactions) - .filter(paymentTransaction -> !paymentTransaction.getState() - .equals(PaymentTransaction.PaymentState.APPROVED)) - .toList()); + private BuyTransaction mapTransaction(Transaction transaction) { + return new BuyTransaction(transaction.getKey(), transaction.getTransactionBuilder(), + mapState(transaction.getStatus()), transaction.getTransactionHash()); } - public Completable remove(String key) { - return cache.remove(key); + private Status mapState(Transaction.Status status) { + Status toReturn; + switch (status) { + case PENDING: + toReturn = Status.PENDING; + break; + case PROCESSING: + toReturn = Status.BUYING; + break; + case COMPLETED: + toReturn = Status.BOUGHT; + break; + default: + case ERROR: + toReturn = Status.ERROR; + break; + case WRONG_NETWORK: + toReturn = Status.WRONG_NETWORK; + break; + case NONCE_ERROR: + toReturn = Status.NONCE_ERROR; + break; + case UNKNOWN_TOKEN: + toReturn = Status.UNKNOWN_TOKEN; + break; + case NO_TOKENS: + toReturn = Status.NO_TOKENS; + break; + case NO_ETHER: + toReturn = Status.NO_ETHER; + break; + case NO_FUNDS: + toReturn = Status.NO_FUNDS; + break; + case NO_INTERNET: + toReturn = Status.NO_INTERNET; + break; + case FORBIDDEN: + toReturn = Status.FORBIDDEN; + break; + } + return toReturn; + } + + public enum Status { + BUYING, BOUGHT, ERROR, WRONG_NETWORK, NONCE_ERROR, UNKNOWN_TOKEN, NO_TOKENS, NO_ETHER, + NO_FUNDS, NO_INTERNET, PENDING, FORBIDDEN + } + + public static class BuyTransaction { + private final String key; + private final TransactionBuilder transactionBuilder; + private final Status status; + private final String transactionHash; + + public BuyTransaction(String key, TransactionBuilder transactionBuilder, Status status, + String transactionHash) { + this.key = key; + this.transactionBuilder = transactionBuilder; + this.status = status; + this.transactionHash = transactionHash; + } + + public String getKey() { + return key; + } + + public TransactionBuilder getTransactionBuilder() { + return transactionBuilder; + } + + public Status getStatus() { + return status; + } + + public String getTransactionHash() { + return transactionHash; + } } } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/BuyTransactionValidatorBds.java b/app/src/main/java/com/asfoundation/wallet/repository/BuyTransactionValidatorBds.java new file mode 100644 index 00000000000..ce2eb4585f5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/BuyTransactionValidatorBds.java @@ -0,0 +1,44 @@ +package com.asfoundation.wallet.repository; + +import com.appcoins.wallet.bdsbilling.BillingPaymentProofSubmission; +import com.appcoins.wallet.bdsbilling.PaymentProof; +import com.asfoundation.wallet.billing.partners.AddressService; +import com.asfoundation.wallet.interact.DefaultTokenProvider; +import com.asfoundation.wallet.interact.SendTransactionInteract; +import io.reactivex.Completable; +import io.reactivex.Single; + +public class BuyTransactionValidatorBds implements TransactionValidator { + private final SendTransactionInteract sendTransactionInteract; + private final BillingPaymentProofSubmission billingPaymentProofSubmission; + private final DefaultTokenProvider defaultTokenProvider; + private final AddressService partnerAddressService; + + public BuyTransactionValidatorBds(SendTransactionInteract sendTransactionInteract, + BillingPaymentProofSubmission billingPaymentProofSubmission, + DefaultTokenProvider defaultTokenProvider, AddressService partnerAddressService) { + this.sendTransactionInteract = sendTransactionInteract; + this.billingPaymentProofSubmission = billingPaymentProofSubmission; + this.defaultTokenProvider = defaultTokenProvider; + this.partnerAddressService = partnerAddressService; + } + + @Override public Completable validate(PaymentTransaction paymentTransaction) { + String packageName = paymentTransaction.getPackageName(); + String productName = paymentTransaction.getTransactionBuilder() + .getSkuId(); + Single getTransactionHash = defaultTokenProvider.getDefaultToken() + .flatMap(tokenInfo -> sendTransactionInteract.computeBuyTransactionHash( + paymentTransaction.getTransactionBuilder())); + Single getStoreAddress = + partnerAddressService.getStoreAddressForPackage(paymentTransaction.getPackageName()); + Single getOemAddress = + partnerAddressService.getOemAddressForPackage(paymentTransaction.getPackageName()); + + return Single.zip(getTransactionHash, getStoreAddress, getOemAddress, + (hash, storeAddress, oemAddress) -> new PaymentProof("appcoins", + paymentTransaction.getApproveHash(), hash, productName, packageName, storeAddress, + oemAddress)) + .flatMapCompletable(billingPaymentProofSubmission::processPurchaseProof); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/CurrencyConversionService.java b/app/src/main/java/com/asfoundation/wallet/repository/CurrencyConversionService.java new file mode 100644 index 00000000000..37e3ac4223e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/CurrencyConversionService.java @@ -0,0 +1,31 @@ +package com.asfoundation.wallet.repository; + +import com.asfoundation.wallet.service.LocalCurrencyConversionService; +import com.asfoundation.wallet.service.TokenRateService; +import com.asfoundation.wallet.ui.iab.FiatValue; +import io.reactivex.Single; + +/** + * Created by franciscocalado on 24/07/2018. + */ + +public class CurrencyConversionService { + + private final TokenRateService tokenRateService; + private final LocalCurrencyConversionService localCurrencyConversionService; + + public CurrencyConversionService(TokenRateService tokenRateService, + LocalCurrencyConversionService localCurrencyConversionService) { + this.tokenRateService = tokenRateService; + this.localCurrencyConversionService = localCurrencyConversionService; + } + + public Single getTokenValue(String currency) { + return tokenRateService.getAppcRate(currency); + } + + public Single getLocalFiatAmount(String appcValue) { + return localCurrencyConversionService.getAppcToLocalFiat(appcValue, 18) + .firstOrError(); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/ErrorMapper.java b/app/src/main/java/com/asfoundation/wallet/repository/ErrorMapper.java deleted file mode 100644 index a19c77451a7..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/ErrorMapper.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.asfoundation.wallet.repository; - -import com.asfoundation.wallet.util.UnknownTokenException; -import java.net.UnknownHostException; - -/** - * Created by trinkes on 20/03/2018. - */ - -public class ErrorMapper { - - public static final String INSUFFICIENT_ERROR_MESSAGE = - "insufficient funds for gas * price + value"; - public static final String NONCE_TOO_LOW_ERROR_MESSAGE = "nonce too low"; - - public PaymentTransaction.PaymentState map(Throwable throwable) { - if (throwable instanceof UnknownHostException) { - return PaymentTransaction.PaymentState.NO_INTERNET; - } - if (throwable instanceof WrongNetworkException) { - return PaymentTransaction.PaymentState.WRONG_NETWORK; - } - if (throwable instanceof TransactionNotFoundException) { - return PaymentTransaction.PaymentState.ERROR; - } - if (throwable instanceof UnknownTokenException) { - return PaymentTransaction.PaymentState.UNKNOWN_TOKEN; - } - if (throwable instanceof TransactionException) { - switch (throwable.getMessage()) { - case INSUFFICIENT_ERROR_MESSAGE: - return PaymentTransaction.PaymentState.NO_FUNDS; - case NONCE_TOO_LOW_ERROR_MESSAGE: - return PaymentTransaction.PaymentState.NONCE_ERROR; - } - } - return PaymentTransaction.PaymentState.ERROR; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/ErrorMapper.kt b/app/src/main/java/com/asfoundation/wallet/repository/ErrorMapper.kt new file mode 100644 index 00000000000..319a7b2aa09 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/ErrorMapper.kt @@ -0,0 +1,45 @@ +package com.asfoundation.wallet.repository + +import com.appcoins.wallet.appcoins.rewards.getMessage +import com.asfoundation.wallet.repository.PaymentTransaction.PaymentState +import com.asfoundation.wallet.util.UnknownTokenException +import retrofit2.HttpException +import java.net.UnknownHostException + +class ErrorMapper { + fun map(throwable: Throwable): PaymentError { + throwable.printStackTrace() + return when (throwable) { + is HttpException -> mapHttpException(throwable) + is UnknownHostException -> PaymentError(PaymentState.NO_INTERNET) + is WrongNetworkException -> PaymentError(PaymentState.WRONG_NETWORK) + is TransactionNotFoundException -> PaymentError(PaymentState.ERROR) + is UnknownTokenException -> PaymentError(PaymentState.UNKNOWN_TOKEN) + is TransactionException -> mapTransactionException(throwable) + else -> PaymentError(PaymentState.ERROR, null, throwable.message) + } + } + + private fun mapTransactionException(throwable: Throwable): PaymentError { + return when (throwable.message) { + INSUFFICIENT_ERROR_MESSAGE -> PaymentError(PaymentState.NO_FUNDS) + NONCE_TOO_LOW_ERROR_MESSAGE -> PaymentError(PaymentState.NONCE_ERROR) + else -> PaymentError(PaymentState.ERROR, null, throwable.message) + } + } + + private fun mapHttpException(exception: HttpException): PaymentError { + return if (exception.code() == FORBIDDEN_CODE) { + PaymentError(PaymentState.FORBIDDEN) + } else { + val message = exception.getMessage() + PaymentError(PaymentState.ERROR, exception.code(), message) + } + } + + companion object { + private const val INSUFFICIENT_ERROR_MESSAGE = "insufficient funds for gas * price + value" + private const val NONCE_TOO_LOW_ERROR_MESSAGE = "nonce too low" + private const val FORBIDDEN_CODE = 403 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/EthereumNetworkRepository.java b/app/src/main/java/com/asfoundation/wallet/repository/EthereumNetworkRepository.java deleted file mode 100644 index 131e1739ba7..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/EthereumNetworkRepository.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.asfoundation.wallet.repository; - -import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.Ticker; -import com.asfoundation.wallet.service.TickerService; -import io.reactivex.Single; -import java.util.HashSet; -import java.util.Set; - -import static com.asfoundation.wallet.C.CLASSIC_NETWORK_NAME; -import static com.asfoundation.wallet.C.ETC_SYMBOL; -import static com.asfoundation.wallet.C.ETHEREUM_NETWORK_NAME; -import static com.asfoundation.wallet.C.ETH_SYMBOL; -import static com.asfoundation.wallet.C.KOVAN_NETWORK_NAME; -import static com.asfoundation.wallet.C.POA_NETWORK_NAME; -import static com.asfoundation.wallet.C.POA_SYMBOL; -import static com.asfoundation.wallet.C.ROPSTEN_NETWORK_NAME; -import static com.asfoundation.wallet.C.SOKOL_NETWORK_NAME; - -public class EthereumNetworkRepository implements EthereumNetworkRepositoryType { - - private final NetworkInfo[] NETWORKS = new NetworkInfo[] { - new NetworkInfo(ETHEREUM_NETWORK_NAME, ETH_SYMBOL, - "https://mainnet.infura.io/llyrtzQ3YhkdESt2Fzrk", "https://api.trustwalletapp.com/", - "https://etherscan.io/tx/", 1, true), - new NetworkInfo(CLASSIC_NETWORK_NAME, ETC_SYMBOL, "https://mewapi.epool.io/", - "https://classic.trustwalletapp.com", "https://gastracker.io/tx/", 61, true), - new NetworkInfo(POA_NETWORK_NAME, POA_SYMBOL, "https://core.poa.network/", - "https://poa.trustwalletapp.com", "https://poaexplorer.com/txid/search/", 99, false), - new NetworkInfo(KOVAN_NETWORK_NAME, ETH_SYMBOL, - "https://kovan.infura.io/llyrtzQ3YhkdESt2Fzrk", "https://kovan.trustwalletapp.com/", - "https://kovan.etherscan.io/tx/", 42, false), - new NetworkInfo(ROPSTEN_NETWORK_NAME, ETH_SYMBOL, - "https://ropsten.infura.io/llyrtzQ3YhkdESt2Fzrk", "https://ropsten.trustwalletapp.com/", - "https://ropsten.etherscan.io/tx/", 3, false), - new NetworkInfo(SOKOL_NETWORK_NAME, POA_SYMBOL, "https://sokol.poa.network", - "https://trust-sokol.herokuapp.com/", "https://sokol-explorer.poa.network/account/", 77, - false), - }; - - private final PreferenceRepositoryType preferences; - private final TickerService tickerService; - private final Set onNetworkChangedListeners = new HashSet<>(); - private NetworkInfo defaultNetwork; - - public EthereumNetworkRepository(PreferenceRepositoryType preferenceRepository, - TickerService tickerService) { - this.preferences = preferenceRepository; - this.tickerService = tickerService; - defaultNetwork = getByName(preferences.getDefaultNetwork()); - if (defaultNetwork == null) { - defaultNetwork = NETWORKS[0]; - } - } - - private NetworkInfo getByName(String name) { - if (name != null && !name.isEmpty()) { - for (NetworkInfo NETWORK : NETWORKS) { - if (name.equals(NETWORK.name)) { - return NETWORK; - } - } - } - return null; - } - - @Override public NetworkInfo getDefaultNetwork() { - return defaultNetwork; - } - - @Override public void setDefaultNetworkInfo(NetworkInfo networkInfo) { - defaultNetwork = networkInfo; - preferences.setDefaultNetwork(defaultNetwork.name); - - for (OnNetworkChangeListener listener : onNetworkChangedListeners) { - listener.onNetworkChanged(networkInfo); - } - } - - @Override public NetworkInfo[] getAvailableNetworkList() { - return NETWORKS; - } - - @Override public void addOnChangeDefaultNetwork(OnNetworkChangeListener onNetworkChanged) { - onNetworkChangedListeners.add(onNetworkChanged); - } - - @Override public Single getTicker() { - return Single.fromObservable(tickerService.fetchTickerPrice(getDefaultNetwork().symbol)); - } - - /** - * execute a single on a specific network and after terminate, restore the network to the - * previous one - * use it only when doing fast operations! - * - * @param chainId - identifies the network where the single should run - * @param single - single to run - */ - @Override public Single executeOnNetworkAndRestore(int chainId, Single single) { - return Single.just(getDefaultNetwork()) - .doOnSuccess(__ -> setDefaultNetworkInfo(chainId)) - .flatMap(defaultNetworkInfo -> single.doAfterTerminate( - () -> setDefaultNetworkInfo(defaultNetworkInfo))); - } - - @Override public void setDefaultNetworkInfo(int chainId) { - setDefaultNetworkInfo(getNetwork(chainId)); - } - - @Override public NetworkInfo getNetwork(int chainId) { - for (NetworkInfo networkInfo : getAvailableNetworkList()) { - if (chainId == networkInfo.chainId) { - return networkInfo; - } - } - throw new IllegalArgumentException("Unknown chain Id: " + chainId); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/EthereumNetworkRepositoryType.java b/app/src/main/java/com/asfoundation/wallet/repository/EthereumNetworkRepositoryType.java deleted file mode 100644 index 4ba430aa097..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/EthereumNetworkRepositoryType.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.asfoundation.wallet.repository; - -import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.Ticker; -import io.reactivex.Single; - -public interface EthereumNetworkRepositoryType { - - NetworkInfo getDefaultNetwork(); - - void setDefaultNetworkInfo(NetworkInfo networkInfo); - - NetworkInfo[] getAvailableNetworkList(); - - void addOnChangeDefaultNetwork(OnNetworkChangeListener onNetworkChanged); - - Single getTicker(); - - Single executeOnNetworkAndRestore(int chainId, Single single); - - void setDefaultNetworkInfo(int chainId); - - NetworkInfo getNetwork(int chainId); -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/EthereumService.java b/app/src/main/java/com/asfoundation/wallet/repository/EthereumService.java index c9bf2559da1..05de327a3fa 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/EthereumService.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/EthereumService.java @@ -7,8 +7,6 @@ * Created by trinkes on 26/02/2018. */ -interface EthereumService { +public interface EthereumService { Single getTransaction(String hash); - - Single getTransaction(String hash, int chainId); } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/GasSettingsRepository.java b/app/src/main/java/com/asfoundation/wallet/repository/GasSettingsRepository.java deleted file mode 100644 index b2960663b4d..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/GasSettingsRepository.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.asfoundation.wallet.repository; - -import com.asfoundation.wallet.entity.GasSettings; -import io.reactivex.Observable; -import io.reactivex.Single; -import java.math.BigDecimal; -import java.util.concurrent.TimeUnit; -import org.web3j.protocol.Web3j; -import org.web3j.protocol.Web3jFactory; -import org.web3j.protocol.core.methods.response.EthGasPrice; -import org.web3j.protocol.http.HttpService; - -import static com.asfoundation.wallet.C.DEFAULT_GAS_LIMIT; -import static com.asfoundation.wallet.C.DEFAULT_GAS_LIMIT_FOR_TOKENS; -import static com.asfoundation.wallet.C.DEFAULT_GAS_PRICE; - -public class GasSettingsRepository implements GasSettingsRepositoryType { - - private final static long FETCH_GAS_PRICE_INTERVAL = 60; - private final EthereumNetworkRepositoryType networkRepository; - private BigDecimal cachedGasPrice; - - public GasSettingsRepository(EthereumNetworkRepositoryType networkRepository) { - this.networkRepository = networkRepository; - - cachedGasPrice = new BigDecimal(DEFAULT_GAS_PRICE); - Observable.interval(0, FETCH_GAS_PRICE_INTERVAL, TimeUnit.SECONDS) - .doOnNext(l -> updateGasSettings()) - .subscribe(l -> { - }, t -> { - }); - } - - private void updateGasSettings() { - final Web3j web3j = - Web3jFactory.build(new HttpService(networkRepository.getDefaultNetwork().rpcServerUrl)); - try { - EthGasPrice price = web3j.ethGasPrice() - .send(); - cachedGasPrice = new BigDecimal(price.getGasPrice()); - } catch (Exception ex) { /* Quietly */ } - } - - public Single getGasSettings(boolean forTokenTransfer) { - return Single.fromCallable(() -> { - BigDecimal gasLimit = forTokenTransfer ? new BigDecimal(DEFAULT_GAS_LIMIT_FOR_TOKENS) - : new BigDecimal(DEFAULT_GAS_LIMIT); - if (cachedGasPrice == null) { - updateGasSettings(); - } - return new GasSettings(cachedGasPrice, gasLimit); - }); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/GasSettingsRepository.kt b/app/src/main/java/com/asfoundation/wallet/repository/GasSettingsRepository.kt new file mode 100644 index 00000000000..da9467342e6 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/GasSettingsRepository.kt @@ -0,0 +1,62 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.entity.GasSettings +import com.asfoundation.wallet.service.GasService +import io.reactivex.Single +import java.math.BigDecimal +import java.util.concurrent.TimeUnit + +class GasSettingsRepository(private val gasService: GasService) : GasSettingsRepositoryType { + + private var lastFlushTime = 0L + private var cachedGasPrice: BigDecimal? = null + + override fun getGasSettings(forTokenTransfer: Boolean): Single { + return getGasPrice() + .map { GasSettings(it, getGasLimit(forTokenTransfer)) } + } + + private fun getGasPriceNetwork(): Single { + return gasService.getGasPrice() + .map { BigDecimal(it.price) } + .doOnSuccess { + cachedGasPrice = it + lastFlushTime = System.nanoTime() + } + .onErrorReturn { + if (cachedGasPrice == null) { + BigDecimal(DEFAULT_GAS_PRICE) + } else { + cachedGasPrice + } + } + } + + private fun shouldRefresh() = + System.nanoTime() - lastFlushTime >= TimeUnit.MINUTES.toNanos(1) || cachedGasPrice == null + + private fun getGasPriceLocal(): Single = Single.just(cachedGasPrice) + + private fun getGasPrice(): Single { + return if (shouldRefresh()) { + getGasPriceNetwork() + } else { + getGasPriceLocal() + } + } + + private fun getGasLimit(forTokenTransfer: Boolean): BigDecimal { + return if (forTokenTransfer) { + BigDecimal(DEFAULT_GAS_LIMIT_FOR_TOKENS) + } else { + BigDecimal(DEFAULT_GAS_LIMIT) + } + } + + companion object { + const val DEFAULT_GAS_LIMIT = "90000" + const val DEFAULT_GAS_LIMIT_FOR_TOKENS = "144000" + const val DEFAULT_GAS_PRICE = "30000000000" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/GasSettingsRepositoryType.java b/app/src/main/java/com/asfoundation/wallet/repository/GasSettingsRepositoryType.java deleted file mode 100644 index ba157e287ec..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/GasSettingsRepositoryType.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.asfoundation.wallet.repository; - -import com.asfoundation.wallet.entity.GasSettings; -import io.reactivex.Single; - -public interface GasSettingsRepositoryType { - Single getGasSettings(boolean forTokenTransfer); -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/GasSettingsRepositoryType.kt b/app/src/main/java/com/asfoundation/wallet/repository/GasSettingsRepositoryType.kt new file mode 100644 index 00000000000..d7f56cb2ebc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/GasSettingsRepositoryType.kt @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.entity.GasSettings +import io.reactivex.Single + +interface GasSettingsRepositoryType { + fun getGasSettings(forTokenTransfer: Boolean): Single +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/InAppPurchaseService.java b/app/src/main/java/com/asfoundation/wallet/repository/InAppPurchaseService.java index 3bbfeb9e43c..818d2238163 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/InAppPurchaseService.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/InAppPurchaseService.java @@ -1,9 +1,19 @@ package com.asfoundation.wallet.repository; +import androidx.annotation.NonNull; +import com.appcoins.wallet.commons.Repository; +import com.asfoundation.wallet.entity.TransactionBuilder; +import com.asfoundation.wallet.interact.GetDefaultWalletBalanceInteract.BalanceState; +import com.asfoundation.wallet.repository.ApproveService.Status; import io.reactivex.Completable; import io.reactivex.Observable; +import io.reactivex.Scheduler; +import io.reactivex.Single; +import java.math.BigDecimal; import java.util.List; +import static com.asfoundation.wallet.interact.GetDefaultWalletBalanceInteract.BalanceState.OK; + /** * Created by trinkes on 13/03/2018. */ @@ -12,25 +22,79 @@ public class InAppPurchaseService { private final Repository cache; private final ApproveService approveService; + private final AllowanceService allowanceService; private final BuyService buyService; - private final NonceGetter nonceGetter; private final BalanceService balanceService; + private final Scheduler scheduler; + private final ErrorMapper errorMapper; public InAppPurchaseService(Repository cache, - ApproveService approveService, BuyService buyService, NonceGetter nonceGetter, - BalanceService balanceService) { + ApproveService approveService, AllowanceService allowanceService, BuyService buyService, + BalanceService balanceService, Scheduler scheduler, ErrorMapper errorMapper) { this.cache = cache; this.approveService = approveService; + this.allowanceService = allowanceService; this.buyService = buyService; - this.nonceGetter = nonceGetter; this.balanceService = balanceService; + this.scheduler = scheduler; + this.errorMapper = errorMapper; } public Completable send(String key, PaymentTransaction paymentTransaction) { + return checkFunds(key, paymentTransaction, checkAllowance(key, paymentTransaction)); + } + + private Completable checkAllowance(String key, PaymentTransaction paymentTransaction) { + TransactionBuilder transactionBuilder = paymentTransaction.getTransactionBuilder(); + String fromAddress = transactionBuilder.fromAddress(); + String contractAddress = transactionBuilder.getIabContract(); + String tokenAddress = transactionBuilder.contractAddress(); + + return allowanceService.checkAllowance(fromAddress, contractAddress, tokenAddress) + .flatMapCompletable(allowance -> { + + if (allowance.compareTo(BigDecimal.ZERO) == 0) { + return approveService.approve(key, paymentTransaction); + } else { + PaymentTransaction approveWithZeroPaymentTransaction = + createApproveZeroTransaction(paymentTransaction); + + return approveService.approveWithoutValidation(key + "zero", + approveWithZeroPaymentTransaction.getTransactionBuilder()) + .andThen(approveService.getApprove(key + "zero") + .filter(approveTransaction -> approveTransaction.getStatus() == Status.APPROVED) + .take(1) + .ignoreElements()) + .andThen(approveService.approve(key, paymentTransaction)); + } + }); + } + + private PaymentTransaction createApproveZeroTransaction(PaymentTransaction paymentTransaction) { + TransactionBuilder transactionBuilder = paymentTransaction.getTransactionBuilder(); + + TransactionBuilder approveWithZeroTransactionBuilder = + copyTransactionBuilder(transactionBuilder); + approveWithZeroTransactionBuilder.amount(BigDecimal.ZERO); + + return new PaymentTransaction(paymentTransaction, approveWithZeroTransactionBuilder); + } + + private TransactionBuilder copyTransactionBuilder(TransactionBuilder transactionBuilder) { + return new TransactionBuilder(transactionBuilder); + } + + public Completable resume(String key, PaymentTransaction paymentTransaction) { + return checkFunds(key, paymentTransaction, buyService.buy(key, paymentTransaction)); + } + + private Completable checkFunds(String key, PaymentTransaction paymentTransaction, + Completable action) { return Completable.fromAction(() -> cache.saveSync(key, paymentTransaction)) .andThen(balanceService.hasEnoughBalance(paymentTransaction.getTransactionBuilder(), paymentTransaction.getTransactionBuilder() .gasSettings().gasLimit) + .observeOn(scheduler) .flatMapCompletable(balance -> { switch (balance) { case NO_TOKEN: @@ -44,12 +108,117 @@ public Completable send(String key, PaymentTransaction paymentTransaction) { PaymentTransaction.PaymentState.NO_FUNDS)); case OK: default: - return cache.save(key, paymentTransaction) - .andThen(nonceGetter.getNonce() - .flatMapCompletable(nonce -> approveService.approve(key, - new PaymentTransaction(paymentTransaction, nonce)))); + return action; } - })); + })) + .onErrorResumeNext(throwable -> { + PaymentError paymentError = errorMapper.map(throwable); + return cache.save(paymentTransaction.getUri(), + new PaymentTransaction(paymentTransaction, paymentError.getPaymentState(), + paymentError.getErrorCode(), paymentError.getErrorMessage())); + }); + } + + private Single mapTransactionToPaymentTransaction( + ApproveService.ApproveTransaction approveTransaction) { + return cache.get(approveTransaction.getKey()) + .firstOrError() + .map(paymentTransaction -> new PaymentTransaction(paymentTransaction, + getStatus(approveTransaction.getStatus()), approveTransaction.getTransactionHash())); + } + + private PaymentTransaction.PaymentState getStatus(ApproveService.Status status) { + PaymentTransaction.PaymentState toReturn; + switch (status) { + case PENDING: + toReturn = PaymentTransaction.PaymentState.PENDING; + break; + case APPROVING: + toReturn = PaymentTransaction.PaymentState.APPROVING; + break; + case APPROVED: + toReturn = PaymentTransaction.PaymentState.APPROVED; + break; + default: + case ERROR: + toReturn = PaymentTransaction.PaymentState.ERROR; + break; + case WRONG_NETWORK: + toReturn = PaymentTransaction.PaymentState.WRONG_NETWORK; + break; + case NONCE_ERROR: + toReturn = PaymentTransaction.PaymentState.NONCE_ERROR; + break; + case UNKNOWN_TOKEN: + toReturn = PaymentTransaction.PaymentState.UNKNOWN_TOKEN; + break; + case NO_TOKENS: + toReturn = PaymentTransaction.PaymentState.NO_TOKENS; + break; + case NO_ETHER: + toReturn = PaymentTransaction.PaymentState.NO_ETHER; + break; + case NO_FUNDS: + toReturn = PaymentTransaction.PaymentState.NO_FUNDS; + break; + case NO_INTERNET: + toReturn = PaymentTransaction.PaymentState.NO_INTERNET; + break; + case FORBIDDEN: + toReturn = PaymentTransaction.PaymentState.FORBIDDEN; + break; + } + return toReturn; + } + + @NonNull private PaymentTransaction map(PaymentTransaction paymentTransaction, + BuyService.BuyTransaction buyTransaction) { + return new PaymentTransaction(paymentTransaction, getStatus(buyTransaction.getStatus()), + paymentTransaction.getApproveHash(), buyTransaction.getTransactionHash()); + } + + private PaymentTransaction.PaymentState getStatus(BuyService.Status status) { + PaymentTransaction.PaymentState paymentState; + switch (status) { + case BUYING: + paymentState = PaymentTransaction.PaymentState.BUYING; + break; + case BOUGHT: + paymentState = PaymentTransaction.PaymentState.BOUGHT; + break; + default: + case ERROR: + paymentState = PaymentTransaction.PaymentState.ERROR; + break; + case WRONG_NETWORK: + paymentState = PaymentTransaction.PaymentState.WRONG_NETWORK; + break; + case NONCE_ERROR: + paymentState = PaymentTransaction.PaymentState.NONCE_ERROR; + break; + case UNKNOWN_TOKEN: + paymentState = PaymentTransaction.PaymentState.UNKNOWN_TOKEN; + break; + case NO_TOKENS: + paymentState = PaymentTransaction.PaymentState.NO_TOKENS; + break; + case NO_ETHER: + paymentState = PaymentTransaction.PaymentState.NO_ETHER; + break; + case NO_FUNDS: + paymentState = PaymentTransaction.PaymentState.NO_FUNDS; + break; + case NO_INTERNET: + paymentState = PaymentTransaction.PaymentState.NO_INTERNET; + break; + case PENDING: + paymentState = PaymentTransaction.PaymentState.PENDING; + break; + case FORBIDDEN: + paymentState = PaymentTransaction.PaymentState.FORBIDDEN; + break; + } + return paymentState; } public void start() { @@ -57,25 +226,41 @@ public void start() { buyService.start(); approveService.getAll() .flatMapCompletable(paymentTransactions -> Observable.fromIterable(paymentTransactions) - .flatMapCompletable( + .flatMapCompletable(approveTransaction -> mapTransactionToPaymentTransaction( + approveTransaction).flatMap( paymentTransaction -> cache.save(paymentTransaction.getUri(), paymentTransaction) - .toSingleDefault(paymentTransaction) - .filter(transaction -> transaction.getState() - .equals(PaymentTransaction.PaymentState.APPROVED)) - .flatMapCompletable( - transaction -> buyService.buy(transaction.getUri(), transaction)))) + .toSingleDefault(paymentTransaction)) + .filter(transaction -> transaction.getState() + .equals(PaymentTransaction.PaymentState.APPROVED)) + .flatMapCompletable(transaction -> { + String uri = transaction.getUri(); + return transaction.getTransactionBuilder() + .amount() + .equals(BigDecimal.ZERO) ? approveService.remove(uri) + : approveService.remove(uri) + .andThen(buyService.buy(uri, transaction) + .onErrorResumeNext(throwable -> { + PaymentError paymentError = errorMapper.map(throwable); + return cache.save(uri, new PaymentTransaction(transaction, + paymentError.getPaymentState(), paymentError.getErrorCode(), + paymentError.getErrorMessage())); + })); + }))) .subscribe(); buyService.getAll() .flatMapCompletable(paymentTransactions -> Observable.fromIterable(paymentTransactions) - .flatMapCompletable( - paymentTransaction -> cache.save(paymentTransaction.getUri(), paymentTransaction) - .toSingleDefault(paymentTransaction) - .filter(transaction -> transaction.getState() - .equals(PaymentTransaction.PaymentState.BOUGHT)) - .flatMapCompletable(transaction -> cache.save(transaction.getUri(), - new PaymentTransaction(paymentTransaction, - PaymentTransaction.PaymentState.COMPLETED))))) + .flatMapCompletable(buyTransaction -> cache.get(buyTransaction.getKey()) + .firstOrError() + .map(paymentTransaction -> map(paymentTransaction, buyTransaction)) + .flatMap( + paymentTransaction -> cache.save(buyTransaction.getKey(), paymentTransaction) + .toSingleDefault(paymentTransaction)) + .filter(transaction -> transaction.getState() + .equals(PaymentTransaction.PaymentState.BOUGHT)) + .flatMapCompletable(transaction -> buyService.remove(transaction.getUri()) + .andThen(cache.save(transaction.getUri(), new PaymentTransaction(transaction, + PaymentTransaction.PaymentState.COMPLETED)))))) .subscribe(); } @@ -92,4 +277,21 @@ public Completable remove(String key) { public Observable> getAll() { return cache.getAll(); } + + public Single hasBalanceToBuy(TransactionBuilder transactionBuilder) { + return balanceService.hasEnoughBalance(transactionBuilder, + transactionBuilder.gasSettings().gasLimit) + .flatMap(balanceState -> { + if (balanceState.equals(OK)) { + return Single.just(true); + } else { + return Single.just(false); + } + }); + } + + public Single getBalanceState(TransactionBuilder transactionBuilder) { + return balanceService.hasEnoughBalance(transactionBuilder, + transactionBuilder.gasSettings().gasLimit); + } } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/IpCountryCodeProvider.java b/app/src/main/java/com/asfoundation/wallet/repository/IpCountryCodeProvider.java new file mode 100644 index 00000000000..08bfd9639a6 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/IpCountryCodeProvider.java @@ -0,0 +1,41 @@ +package com.asfoundation.wallet.repository; + +import com.asfoundation.wallet.poa.CountryCodeProvider; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.reactivex.Single; +import retrofit2.http.GET; + +public class IpCountryCodeProvider implements CountryCodeProvider { + public static String ENDPOINT = com.asf.wallet.BuildConfig.BACKEND_HOST; + private final IpApi ipApi; + + public IpCountryCodeProvider(IpApi ipApi) { + this.ipApi = ipApi; + } + + @Override public Single getCountryCode() { + return ipApi.myIp() + .map(IpResponse::getCountryCode); + } + + public interface IpApi { + @GET("exchange/countrycode") Single myIp(); + } + + @JsonInclude(JsonInclude.Include.NON_NULL) public class IpResponse { + + @JsonProperty("countryCode") private String countryCode; + + public IpResponse() { + } + + public String getCountryCode() { + return countryCode; + } + + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/NoValidateTransactionValidator.java b/app/src/main/java/com/asfoundation/wallet/repository/NoValidateTransactionValidator.java new file mode 100644 index 00000000000..d1e55ff39d0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/NoValidateTransactionValidator.java @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.repository; + +import io.reactivex.Completable; + +public class NoValidateTransactionValidator implements TransactionValidator { + + public NoValidateTransactionValidator() { + } + + @Override public Completable validate(PaymentTransaction paymentTransaction) { + return Completable.complete(); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/NonceGetter.java b/app/src/main/java/com/asfoundation/wallet/repository/NonceGetter.java deleted file mode 100644 index 48ed041643e..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/NonceGetter.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.asfoundation.wallet.repository; - -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; -import java.math.BigInteger; -import org.web3j.protocol.Web3j; -import org.web3j.protocol.Web3jFactory; -import org.web3j.protocol.core.DefaultBlockParameterName; -import org.web3j.protocol.core.methods.response.EthGetTransactionCount; -import org.web3j.protocol.http.HttpService; - -public class NonceGetter { - - private static final int BUMP_TIME_THRESHOLD_IN_MILLIS = 5000; - private final EthereumNetworkRepositoryType networkRepository; - private final FindDefaultWalletInteract defaultWalletInteract; - private boolean shouldBump; - private long timeStamp; - - public NonceGetter(EthereumNetworkRepositoryType networkRepository, - FindDefaultWalletInteract defaultWalletInteract) { - this.networkRepository = networkRepository; - this.defaultWalletInteract = defaultWalletInteract; - } - - Single getNonce(String fromAddress) { - final Web3j web3j = - Web3jFactory.build(new HttpService(networkRepository.getDefaultNetwork().rpcServerUrl)); - return Single.fromCallable(() -> { - EthGetTransactionCount ethGetTransactionCount = - web3j.ethGetTransactionCount(fromAddress, DefaultBlockParameterName.LATEST) - .send(); - BigInteger transactionCount = ethGetTransactionCount.getTransactionCount(); - if (shouldBump && System.currentTimeMillis() - timeStamp < BUMP_TIME_THRESHOLD_IN_MILLIS) { - transactionCount = transactionCount.add(BigInteger.ONE); - shouldBump = false; - } - return transactionCount; - }) - .subscribeOn(Schedulers.io()); - } - - public void bump() { - timeStamp = System.currentTimeMillis(); - shouldBump = true; - } - - public Single getNonce() { - return defaultWalletInteract.find() - .flatMap(wallet -> getNonce(wallet.address)); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/NotTrackTransactionService.java b/app/src/main/java/com/asfoundation/wallet/repository/NotTrackTransactionService.java new file mode 100644 index 00000000000..be265f80d7c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/NotTrackTransactionService.java @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.repository; + +import com.asfoundation.wallet.entity.PendingTransaction; +import io.reactivex.Observable; + +public class NotTrackTransactionService implements TrackTransactionService { + @Override public Observable checkTransactionState(String hash) { + return Observable.just(new PendingTransaction(hash, false)); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/OffChainTransactions.kt b/app/src/main/java/com/asfoundation/wallet/repository/OffChainTransactions.kt new file mode 100644 index 00000000000..a759a409e89 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/OffChainTransactions.kt @@ -0,0 +1,30 @@ +package com.asfoundation.wallet.repository + +import android.annotation.SuppressLint +import com.asfoundation.wallet.transactions.Transaction +import com.asfoundation.wallet.transactions.TransactionsMapper +import retrofit2.HttpException + +class OffChainTransactions(private val repository: OffChainTransactionsRepository, + private val mapper: TransactionsMapper, + private val versionCode: String) { + + fun getTransactions(wallet: String, startingDate: Long? = null, + endingDate: Long? = null, offset: Int, sort: Sort?, + limit: Int = 10): List { + @SuppressLint("DefaultLocale") val lowerCaseSort = sort?.name?.toLowerCase() + val transactions = + repository.getTransactionsSync(wallet, versionCode, startingDate, endingDate, offset, + sort = lowerCaseSort, limit = limit) + .execute() + val body = transactions.body() + if (transactions.isSuccessful && body != null) { + return mapper.mapTransactionsFromWalletHistory(body.result) + } + throw HttpException(transactions) + } + + enum class Sort { + ASC, DESC + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/OffChainTransactionsRepository.kt b/app/src/main/java/com/asfoundation/wallet/repository/OffChainTransactionsRepository.kt new file mode 100644 index 00000000000..6604cf48254 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/OffChainTransactionsRepository.kt @@ -0,0 +1,37 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.entity.WalletHistory +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Query +import java.text.DateFormat +import java.util.* + +class OffChainTransactionsRepository(private val api: TransactionsApi, + private val dateFormatter: DateFormat) { + + + fun getTransactionsSync(wallet: String, versionCode: String, startingDate: Long? = null, + endingDate: Long? = null, offset: Int, sort: String?, + limit: Int): Call { + + return api.transactionHistorySync(wallet, versionCode, + startingDate = startingDate?.let { dateFormatter.format(it) }, + endingDate = endingDate?.let { dateFormatter.format(it) }, offset = offset, sort = sort, + limit = limit, languageCode = Locale.getDefault().language) + } + + interface TransactionsApi { + @GET("appc/wallethistory") + fun transactionHistorySync( + @Query("wallet") wallet: String, + @Query("version_code") versionCode: String, + @Query("type") transactionType: String = "all", + @Query("offset") offset: Int = 0, + @Query("from") startingDate: String? = null, + @Query("to") endingDate: String? = null, + @Query("sort") sort: String? = "desc", + @Query("limit") limit: Int, + @Query("lang_code") languageCode: String): Call + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/OnNetworkChangeListener.java b/app/src/main/java/com/asfoundation/wallet/repository/OnNetworkChangeListener.java deleted file mode 100644 index 341f1fe85fe..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/OnNetworkChangeListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.asfoundation.wallet.repository; - -import com.asfoundation.wallet.entity.NetworkInfo; - -public interface OnNetworkChangeListener { - void onNetworkChanged(NetworkInfo networkInfo); -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/PasswordStore.java b/app/src/main/java/com/asfoundation/wallet/repository/PasswordStore.java index a2b6a00daaf..1d4dd7dc4ed 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/PasswordStore.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/PasswordStore.java @@ -1,13 +1,14 @@ package com.asfoundation.wallet.repository; -import com.asfoundation.wallet.entity.Wallet; import io.reactivex.Completable; import io.reactivex.Single; public interface PasswordStore { - Single getPassword(Wallet wallet); + Single getPassword(String address); - Completable setPassword(Wallet wallet, String password); + Completable setPassword(String address, String password); Single generatePassword(); + + Completable setBackUpPassword(String masterPassword); } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/PaymentError.kt b/app/src/main/java/com/asfoundation/wallet/repository/PaymentError.kt new file mode 100644 index 00000000000..7716d42cb39 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/PaymentError.kt @@ -0,0 +1,4 @@ +package com.asfoundation.wallet.repository + +data class PaymentError(val paymentState: PaymentTransaction.PaymentState, + val errorCode: Int? = null, val errorMessage: String? = null) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/PaymentTransaction.java b/app/src/main/java/com/asfoundation/wallet/repository/PaymentTransaction.java index 169f02bf414..3bd93425578 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/PaymentTransaction.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/PaymentTransaction.java @@ -1,7 +1,7 @@ package com.asfoundation.wallet.repository; import com.asfoundation.wallet.entity.TransactionBuilder; -import java.math.BigInteger; +import java.util.Objects; import javax.annotation.Nullable; /** @@ -9,80 +9,122 @@ */ public class PaymentTransaction { - public static final BigInteger INVALID_NONCE = new BigInteger("-1"); private final String uri; private final @Nullable String approveHash; private final @Nullable String buyHash; private final TransactionBuilder transactionBuilder; private final PaymentState state; - private final BigInteger nonce; private final String packageName; private final String productName; + private final String productId; + private final String developerPayload; + private final String callbackUrl; + private final String orderReference; + private final Integer errorCode; + private final String errorMessage; public PaymentTransaction(String uri, TransactionBuilder transactionBuilder, PaymentState state, - @Nullable String approveHash, @Nullable String buyHash, BigInteger nonce, String packageName, - String productName) { + @Nullable String approveHash, @Nullable String buyHash, String packageName, + String productName, String productId, String developerPayload, String callbackUrl, + @Nullable String orderReference, @Nullable Integer errorCode, @Nullable String errorMessage) { this.uri = uri; this.transactionBuilder = transactionBuilder; this.state = state; this.approveHash = approveHash; this.buyHash = buyHash; - this.nonce = nonce; this.packageName = packageName; this.productName = productName; + this.productId = productId; + this.developerPayload = developerPayload; + this.callbackUrl = callbackUrl; + this.orderReference = orderReference; + this.errorCode = errorCode; + this.errorMessage = errorMessage; } public PaymentTransaction(PaymentTransaction paymentTransaction, PaymentState state) { this(paymentTransaction.getUri(), paymentTransaction.getTransactionBuilder(), state, paymentTransaction.getApproveHash(), paymentTransaction.getBuyHash(), - paymentTransaction.getNonce(), paymentTransaction.getPackageName(), - paymentTransaction.getProductName()); + paymentTransaction.getPackageName(), paymentTransaction.getProductName(), + paymentTransaction.getProductId(), paymentTransaction.getDeveloperPayload(), + paymentTransaction.getCallbackUrl(), paymentTransaction.getOrderReference(), null, null); + } + + public PaymentTransaction(PaymentTransaction paymentTransaction, PaymentState state, + Integer errorCode, String errorMessage) { + this(paymentTransaction.getUri(), paymentTransaction.getTransactionBuilder(), state, + paymentTransaction.getApproveHash(), paymentTransaction.getBuyHash(), + paymentTransaction.getPackageName(), paymentTransaction.getProductName(), + paymentTransaction.getProductId(), paymentTransaction.getDeveloperPayload(), + paymentTransaction.getCallbackUrl(), paymentTransaction.getOrderReference(), errorCode, + errorMessage); } public PaymentTransaction(String uri, TransactionBuilder transactionBuilder, PaymentState state, - @Nullable String approveHash, String packageName, String productName) { + @Nullable String approveHash, String packageName, String productName, String productId, + String developerPayload, String callbackUrl, String orderReference) { this.approveHash = approveHash; this.packageName = packageName; this.uri = uri; this.transactionBuilder = transactionBuilder; this.state = state; this.productName = productName; - buyHash = null; - nonce = INVALID_NONCE; + this.productId = productId; + this.buyHash = null; + this.developerPayload = developerPayload; + this.callbackUrl = callbackUrl; + this.orderReference = orderReference; + this.errorCode = null; + this.errorMessage = null; } public PaymentTransaction(PaymentTransaction paymentTransaction, PaymentState state, String approveHash) { this(paymentTransaction.getUri(), paymentTransaction.getTransactionBuilder(), state, - approveHash, null, paymentTransaction.getNonce(), paymentTransaction.getPackageName(), - paymentTransaction.getProductName()); + approveHash, null, paymentTransaction.getPackageName(), paymentTransaction.getProductName(), + paymentTransaction.getProductId(), paymentTransaction.getDeveloperPayload(), + paymentTransaction.getCallbackUrl(), paymentTransaction.getOrderReference(), null, null); } public PaymentTransaction(PaymentTransaction paymentTransaction, PaymentState state, String approveHash, String buyHash) { this(paymentTransaction.getUri(), paymentTransaction.getTransactionBuilder(), state, - approveHash, buyHash, paymentTransaction.getNonce(), paymentTransaction.getPackageName(), - paymentTransaction.getProductName()); + approveHash, buyHash, paymentTransaction.getPackageName(), + paymentTransaction.getProductName(), paymentTransaction.getProductId(), + paymentTransaction.getDeveloperPayload(), paymentTransaction.getCallbackUrl(), + paymentTransaction.getOrderReference(), null, null); } public PaymentTransaction(String uri, TransactionBuilder transactionBuilder, String packageName, - String productName) { - this(uri, transactionBuilder, PaymentState.PENDING, null, packageName, productName); + String productName, String productId, String developerPayload, String callbackUrl, + String orderReference) { + this(uri, transactionBuilder, PaymentState.PENDING, null, packageName, productName, productId, + developerPayload, callbackUrl, orderReference); } - public PaymentTransaction(PaymentTransaction paymentTransaction, BigInteger nonce) { - this(paymentTransaction.getUri(), paymentTransaction.getTransactionBuilder(), - paymentTransaction.getState(), paymentTransaction.getApproveHash(), - paymentTransaction.getBuyHash(), nonce, paymentTransaction.getPackageName(), - paymentTransaction.getProductName()); + public PaymentTransaction(PaymentTransaction paymentTransaction, + TransactionBuilder transactionBuilder) { + this.uri = paymentTransaction.uri; + this.approveHash = paymentTransaction.approveHash; + this.buyHash = paymentTransaction.buyHash; + this.transactionBuilder = transactionBuilder; + this.state = paymentTransaction.state; + this.packageName = paymentTransaction.packageName; + this.productName = paymentTransaction.productName; + this.productId = paymentTransaction.productId; + this.developerPayload = paymentTransaction.developerPayload; + this.callbackUrl = paymentTransaction.callbackUrl; + this.orderReference = paymentTransaction.orderReference; + this.errorCode = null; + this.errorMessage = null; } - public String getPackageName() { - return packageName; + public String getOrderReference() { + return orderReference; } - public BigInteger getNonce() { - return nonce; + public String getPackageName() { + return packageName; } public String getUri() { @@ -120,11 +162,10 @@ public PaymentState getState() { PaymentTransaction that = (PaymentTransaction) o; if (!uri.equals(that.uri)) return false; - if (approveHash != null ? !approveHash.equals(that.approveHash) : that.approveHash != null) { + if (!Objects.equals(approveHash, that.approveHash)) { return false; } - if (transactionBuilder != null ? !transactionBuilder.equals(that.transactionBuilder) - : that.transactionBuilder != null) { + if (!Objects.equals(transactionBuilder, that.transactionBuilder)) { return false; } return state == that.state; @@ -132,18 +173,33 @@ public PaymentState getState() { @Override public String toString() { return "PaymentTransaction{" - + "approveHash='" + + "uri='" + + uri + + '\'' + + ", approveHash='" + approveHash + '\'' + ", buyHash='" + buyHash + '\'' - + ", state=" - + state + ", transactionBuilder=" + transactionBuilder - + ", uri='" - + uri + + ", state=" + + state + + ", packageName='" + + packageName + + '\'' + + ", productName='" + + productName + + '\'' + + ", productId='" + + productId + + '\'' + + ", developerPayload='" + + developerPayload + + '\'' + + ", callbackUrl='" + + callbackUrl + '\'' + '}'; } @@ -152,8 +208,28 @@ public String getProductName() { return productName; } + public String getProductId() { + return productId; + } + + public String getDeveloperPayload() { + return developerPayload; + } + + public String getCallbackUrl() { + return callbackUrl; + } + + public Integer getErrorCode() { + return errorCode; + } + + public String getErrorMessage() { + return errorMessage; + } + public enum PaymentState { PENDING, APPROVING, APPROVED, BUYING, BOUGHT, COMPLETED, ERROR, WRONG_NETWORK, NONCE_ERROR, - UNKNOWN_TOKEN, NO_TOKENS, NO_ETHER, NO_FUNDS, NO_INTERNET + UNKNOWN_TOKEN, NO_TOKENS, NO_ETHER, NO_FUNDS, NO_INTERNET, FORBIDDEN } } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/PendingTransactionService.java b/app/src/main/java/com/asfoundation/wallet/repository/PendingTransactionService.java index b8c3a154bd3..681ba15583b 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/PendingTransactionService.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/PendingTransactionService.java @@ -9,7 +9,7 @@ * Created by trinkes on 26/02/2018. */ -public class PendingTransactionService { +public class PendingTransactionService implements TrackTransactionService { private final EthereumService service; private final int period; private final Scheduler scheduler; @@ -20,19 +20,11 @@ public PendingTransactionService(EthereumService service, Scheduler scheduler, i this.period = period; } - public Observable checkTransactionState(String hash) { + @Override public Observable checkTransactionState(String hash) { return Observable.interval(period, TimeUnit.SECONDS, scheduler) .timeInterval() .switchMap(scan -> service.getTransaction(hash) .toObservable()) .takeUntil(pendingTransaction -> !pendingTransaction.isPending()); } - - public Observable checkTransactionState(String hash, int chainId) { - return Observable.interval(period, TimeUnit.SECONDS, scheduler) - .timeInterval() - .switchMap(scan -> service.getTransaction(hash, chainId) - .toObservable()) - .takeUntil(pendingTransaction -> !pendingTransaction.isPending()); - } } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/PreferenceRepositoryType.java b/app/src/main/java/com/asfoundation/wallet/repository/PreferenceRepositoryType.java deleted file mode 100644 index 9e67f937773..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/PreferenceRepositoryType.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.asfoundation.wallet.repository; - -public interface PreferenceRepositoryType { - String getCurrentWalletAddress(); - - void setCurrentWalletAddress(String address); - - String getDefaultNetwork(); - - void setDefaultNetwork(String netName); -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/PreferencesRepositoryType.kt b/app/src/main/java/com/asfoundation/wallet/repository/PreferencesRepositoryType.kt new file mode 100644 index 00000000000..db0b9606bf3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/PreferencesRepositoryType.kt @@ -0,0 +1,102 @@ +package com.asfoundation.wallet.repository + +import io.reactivex.Completable +import io.reactivex.Single + +interface PreferencesRepositoryType { + + fun hasCompletedOnboarding(): Boolean + + fun setOnboardingComplete() + + fun hasClickedSkipOnboarding(): Boolean + + fun setOnboardingSkipClicked() + + fun getCurrentWalletAddress(): String? + + fun setCurrentWalletAddress(address: String) + + fun isFirstTimeOnTransactionActivity(): Boolean + + fun setFirstTimeOnTransactionActivity() + + fun getPoaNotificationSeenTime(): Long + + fun clearPoaNotificationSeenTime() + + fun setPoaNotificationSeenTime(currentTimeInMillis: Long) + + fun setWalletValidationStatus(walletAddress: String, validated: Boolean) + + fun isWalletValidated(walletAddress: String): Boolean + + fun removeWalletValidationStatus(walletAddress: String): Completable + + fun saveAutoUpdateCardDismiss(updateVersionCode: Int): Completable + + fun getAutoUpdateCardDismissedVersion(): Single + + fun getUpdateNotificationSeenTime(): Long + + fun setUpdateNotificationSeenTime(currentTimeMillis: Long) + + fun getBackupNotificationSeenTime(walletAddress: String): Long + + fun setBackupNotificationSeenTime(walletAddress: String, currentTimeMillis: Long) + + fun removeBackupNotificationSeenTime(walletAddress: String): Completable + + fun isWalletRestoreBackup(walletAddress: String): Boolean + + fun setWalletRestoreBackup(walletAddress: String) + + fun removeWalletRestoreBackup(walletAddress: String): Completable + + fun hasShownBackup(walletAddress: String): Boolean + + fun setHasShownBackup(walletAddress: String, hasShown: Boolean) + + fun getAndroidId(): String + + fun setAndroidId(androidId: String) + + fun getGamificationLevel(): Int + + fun saveChosenUri(uri: String) + + fun getChosenUri(): String? + + fun getSeenBackupTooltip(): Boolean + + fun saveSeenBackupTooltip() + + fun hasDismissedBackupSystemNotification(walletAddress: String): Boolean + + fun setDismissedBackupSystemNotification(walletAddress: String) + + fun getWalletPurchasesCount(walletAddress: String): Int + + fun incrementWalletPurchasesCount(walletAddress: String, count: Int): Completable + + fun setWalletId(walletId: String) + + fun getWalletId(): String? + + fun setPromotionNotificationSeenTime(walletAddress: String, currentTimeMillis: Long) + + fun removePromotionNotificationSeenTime(walletAddress: String): Completable + + fun showGamificationDisclaimer(): Boolean + + fun setGamificationDisclaimerShown() + + fun setAuthenticationPermission(result: Boolean) + + fun hasAuthenticationPermission(): Boolean + + fun setAuthenticationErrorTime(timer: Long) + + fun getAuthenticationErrorTime(): Long + +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/SharedPreferenceRepository.java b/app/src/main/java/com/asfoundation/wallet/repository/SharedPreferenceRepository.java deleted file mode 100644 index c6570fd01f4..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/SharedPreferenceRepository.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.asfoundation.wallet.repository; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -public class SharedPreferenceRepository implements PreferenceRepositoryType { - - private static final String CURRENT_ACCOUNT_ADDRESS_KEY = "current_account_address"; - private static final String DEFAULT_NETWORK_NAME_KEY = "default_network_name"; - private static final String GAS_PRICE_KEY = "gas_price"; - private static final String GAS_LIMIT_KEY = "gas_limit"; - private static final String GAS_LIMIT_FOR_TOKENS_KEY = "gas_limit_for_tokens"; - - private final SharedPreferences pref; - - public SharedPreferenceRepository(Context context) { - pref = PreferenceManager.getDefaultSharedPreferences(context); - } - - @Override public String getCurrentWalletAddress() { - return pref.getString(CURRENT_ACCOUNT_ADDRESS_KEY, null); - } - - @Override public void setCurrentWalletAddress(String address) { - pref.edit() - .putString(CURRENT_ACCOUNT_ADDRESS_KEY, address) - .apply(); - } - - @Override public String getDefaultNetwork() { - return pref.getString(DEFAULT_NETWORK_NAME_KEY, null); - } - - @Override public void setDefaultNetwork(String netName) { - pref.edit() - .putString(DEFAULT_NETWORK_NAME_KEY, netName) - .apply(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/SharedPreferencesRepository.kt b/app/src/main/java/com/asfoundation/wallet/repository/SharedPreferencesRepository.kt new file mode 100644 index 00000000000..051b5cebbcf --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/SharedPreferencesRepository.kt @@ -0,0 +1,258 @@ +package com.asfoundation.wallet.repository + +import android.content.SharedPreferences +import io.reactivex.Completable +import io.reactivex.Single + +class SharedPreferencesRepository(private val pref: SharedPreferences) : PreferencesRepositoryType { + + companion object { + + private const val CURRENT_ACCOUNT_ADDRESS_KEY = "current_account_address" + private const val ONBOARDING_COMPLETE_KEY = "onboarding_complete" + private const val ONBOARDING_SKIP_CLICKED_KEY = "onboarding_skip_clicked" + private const val FIRST_TIME_ON_TRANSACTION_ACTIVITY_KEY = "first_time_on_transaction_activity" + private const val AUTO_UPDATE_VERSION = "auto_update_version" + private const val POA_LIMIT_SEEN_TIME = "poa_limit_seen_time" + private const val UPDATE_SEEN_TIME = "update_seen_time" + private const val BACKUP_SEEN_TIME = "backup_seen_time_" + private const val PROMOTION_SEEN_TIME = "promotion_seen_time_" + private const val WALLET_VERIFIED = "wallet_verified_" + private const val WALLET_IMPORT_BACKUP = "wallet_import_backup_" + private const val HAS_SHOWN_BACKUP = "has_shown_backup_" + private const val ANDROID_ID = "android_id" + private const val GAMIFICATION_LEVEL = "gamification_level" + private const val KEYSTORE_DIRECTORY = "keystore_directory" + private const val SEEN_BACKUP_TOOLTIP = "seen_backup_tooltip" + private const val SEEN_BACKUP_SYSTEM_NOTIFICATION = "seen_backup_system_notification_" + private const val WALLET_PURCHASES_COUNT = "wallet_purchases_count_" + private const val WALLET_ID = "wallet_id" + private const val SHOW_GAMIFICATION_DISCLAIMER = "SHOW_GAMIFICATION_DISCLAIMER" + private const val AUTHENTICATION_PERMISSION = "authentication_permission" + private const val AUTHENTICATION_ERROR_TIME = "authentication_error_time" + } + + override fun hasCompletedOnboarding() = pref.getBoolean(ONBOARDING_COMPLETE_KEY, false) + + override fun setOnboardingComplete() { + pref.edit() + .putBoolean(ONBOARDING_COMPLETE_KEY, true) + .apply() + } + + override fun hasClickedSkipOnboarding() = pref.getBoolean(ONBOARDING_SKIP_CLICKED_KEY, false) + + override fun setOnboardingSkipClicked() { + pref.edit() + .putBoolean(ONBOARDING_SKIP_CLICKED_KEY, true) + .apply() + } + + override fun getCurrentWalletAddress(): String? { + return pref.getString(CURRENT_ACCOUNT_ADDRESS_KEY, null) + } + + override fun setCurrentWalletAddress(address: String) { + pref.edit() + .putString(CURRENT_ACCOUNT_ADDRESS_KEY, address) + .apply() + } + + override fun isFirstTimeOnTransactionActivity(): Boolean { + return pref.getBoolean(FIRST_TIME_ON_TRANSACTION_ACTIVITY_KEY, false) + } + + override fun setFirstTimeOnTransactionActivity() { + pref.edit() + .putBoolean(FIRST_TIME_ON_TRANSACTION_ACTIVITY_KEY, true) + .apply() + } + + override fun saveAutoUpdateCardDismiss(updateVersionCode: Int): Completable { + return Completable.fromCallable { + pref.edit() + .putInt(AUTO_UPDATE_VERSION, updateVersionCode) + .apply() + } + } + + override fun getAutoUpdateCardDismissedVersion(): Single { + return Single.fromCallable { pref.getInt(AUTO_UPDATE_VERSION, 0) } + } + + override fun clearPoaNotificationSeenTime() { + pref.edit() + .remove(POA_LIMIT_SEEN_TIME) + .apply() + } + + override fun getPoaNotificationSeenTime() = pref.getLong(POA_LIMIT_SEEN_TIME, -1) + + override fun setPoaNotificationSeenTime(currentTimeInMillis: Long) { + pref.edit() + .putLong(POA_LIMIT_SEEN_TIME, currentTimeInMillis) + .apply() + } + + override fun setUpdateNotificationSeenTime(currentTimeMillis: Long) { + pref.edit() + .putLong(UPDATE_SEEN_TIME, currentTimeMillis) + .apply() + } + + override fun getUpdateNotificationSeenTime() = pref.getLong(UPDATE_SEEN_TIME, -1) + + override fun setWalletValidationStatus(walletAddress: String, validated: Boolean) { + pref.edit() + .putBoolean(WALLET_VERIFIED + walletAddress, validated) + .apply() + } + + override fun isWalletValidated(walletAddress: String) = + pref.getBoolean(WALLET_VERIFIED + walletAddress, false) + + override fun removeWalletValidationStatus(walletAddress: String): Completable { + return Completable.fromAction { + pref.edit() + .remove(WALLET_VERIFIED + walletAddress) + .apply() + } + } + + override fun getBackupNotificationSeenTime(walletAddress: String) = + pref.getLong(BACKUP_SEEN_TIME + walletAddress, -1) + + override fun setBackupNotificationSeenTime(walletAddress: String, currentTimeMillis: Long) { + pref.edit() + .putLong(BACKUP_SEEN_TIME + walletAddress, currentTimeMillis) + .apply() + } + + override fun removeBackupNotificationSeenTime(walletAddress: String): Completable { + return Completable.fromAction { + pref.edit() + .remove(BACKUP_SEEN_TIME + walletAddress) + .apply() + } + } + + override fun setPromotionNotificationSeenTime(walletAddress: String, currentTimeMillis: Long) { + pref.edit() + .putLong(PROMOTION_SEEN_TIME + walletAddress, currentTimeMillis) + .apply() + } + + override fun removePromotionNotificationSeenTime(walletAddress: String): Completable { + return Completable.fromAction { + pref.edit() + .remove(PROMOTION_SEEN_TIME + walletAddress) + .apply() + } + } + + override fun isWalletRestoreBackup(walletAddress: String) = + pref.getBoolean(WALLET_IMPORT_BACKUP + walletAddress, false) + + override fun setWalletRestoreBackup(walletAddress: String) { + pref.edit() + .putBoolean(WALLET_IMPORT_BACKUP + walletAddress, true) + .apply() + } + + override fun removeWalletRestoreBackup(walletAddress: String): Completable { + return Completable.fromAction { + pref.edit() + .remove(WALLET_IMPORT_BACKUP + walletAddress) + .apply() + } + } + + override fun hasShownBackup(walletAddress: String): Boolean { + return pref.getBoolean(HAS_SHOWN_BACKUP + walletAddress, false) + } + + override fun setHasShownBackup(walletAddress: String, hasShown: Boolean) { + pref.edit() + .putBoolean(HAS_SHOWN_BACKUP + walletAddress, hasShown) + .apply() + } + + override fun getAndroidId() = pref.getString(ANDROID_ID, "") + .orEmpty() + + + override fun setAndroidId(androidId: String) { + pref.edit() + .putString(ANDROID_ID, androidId) + .apply() + } + + override fun getGamificationLevel() = pref.getInt(GAMIFICATION_LEVEL, -1) + + override fun saveChosenUri(uri: String) { + pref.edit() + .putString(KEYSTORE_DIRECTORY, uri) + .apply() + } + + override fun getChosenUri() = pref.getString(KEYSTORE_DIRECTORY, null) + + override fun getSeenBackupTooltip() = pref.getBoolean(SEEN_BACKUP_TOOLTIP, false) + + override fun saveSeenBackupTooltip() { + pref.edit() + .putBoolean(SEEN_BACKUP_TOOLTIP, true) + .apply() + } + + override fun hasDismissedBackupSystemNotification(walletAddress: String) = + pref.getBoolean(SEEN_BACKUP_SYSTEM_NOTIFICATION + walletAddress, false) + + override fun setDismissedBackupSystemNotification(walletAddress: String) = + pref.edit() + .putBoolean(SEEN_BACKUP_SYSTEM_NOTIFICATION + walletAddress, true) + .apply() + + override fun getWalletPurchasesCount(walletAddress: String) = + pref.getInt(WALLET_PURCHASES_COUNT + walletAddress, 0) + + override fun incrementWalletPurchasesCount(walletAddress: String, count: Int) = + Completable.fromAction { + pref.edit() + .putInt(WALLET_PURCHASES_COUNT + walletAddress, count) + .apply() + } + + override fun setWalletId(walletId: String) { + pref.edit() + .putString(WALLET_ID, walletId) + .apply() + } + + override fun getWalletId() = pref.getString(WALLET_ID, null) + + override fun showGamificationDisclaimer() = pref.getBoolean(SHOW_GAMIFICATION_DISCLAIMER, true) + + override fun setGamificationDisclaimerShown() { + pref.edit() + .putBoolean(SHOW_GAMIFICATION_DISCLAIMER, false) + .apply() + } + override fun setAuthenticationPermission(result: Boolean) { + pref.edit() + .putBoolean(AUTHENTICATION_PERMISSION, result) + .apply() + } + + override fun hasAuthenticationPermission(): Boolean { + return pref.getBoolean(AUTHENTICATION_PERMISSION, false) + } + + override fun setAuthenticationErrorTime(timer: Long) { + pref.edit() + .putLong(AUTHENTICATION_ERROR_TIME, timer) + .apply() + } + + override fun getAuthenticationErrorTime() = pref.getLong(AUTHENTICATION_ERROR_TIME, 0) +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/SignDataStandardNormalizer.java b/app/src/main/java/com/asfoundation/wallet/repository/SignDataStandardNormalizer.java new file mode 100644 index 00000000000..ebbf06b3f55 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/SignDataStandardNormalizer.java @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.repository; + +import com.asfoundation.wallet.service.AccountWalletService; +import org.jetbrains.annotations.NotNull; + +public class SignDataStandardNormalizer implements AccountWalletService.ContentNormalizer { + @NotNull @Override public String normalize(@NotNull String content) { + return "\\x19Ethereum Signed Message:\n" + content.length() + content; + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/SmsValidationRepository.kt b/app/src/main/java/com/asfoundation/wallet/repository/SmsValidationRepository.kt new file mode 100644 index 00000000000..96aa5f0c512 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/SmsValidationRepository.kt @@ -0,0 +1,95 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.entity.WalletStatus +import com.asfoundation.wallet.entity.WalletValidationException +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.service.SmsValidationApi +import com.asfoundation.wallet.util.isNoNetworkException +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import com.google.gson.Gson +import io.reactivex.Single +import retrofit2.HttpException + +class SmsValidationRepository( + private val api: SmsValidationApi, + private val gson: Gson, + private val logger: Logger +) : SmsValidationRepositoryType { + + companion object { + private val TAG = SmsValidationRepository::class.java.simpleName + } + + override fun isValid(walletAddress: String): Single { + return api.isValid(walletAddress) + .map(this::mapValidationResponse) + .onErrorReturn(this::mapError) + } + + override fun requestValidationCode(phoneNumber: String): Single { + return api.requestValidationCode(phoneNumber) + .map { WalletValidationStatus.SUCCESS } + .onErrorReturn(this::mapError) + } + + override fun validateCode(phoneNumber: String, walletAddress: String, + validationCode: String): Single { + return api.validateCode(phoneNumber, walletAddress, validationCode) + .map(this::mapCodeValidationResponse) + .onErrorReturn(this::mapError) + } + + private fun mapValidationResponse(walletStatus: WalletStatus): WalletValidationStatus { + return if (walletStatus.verified) WalletValidationStatus.SUCCESS else WalletValidationStatus.GENERIC_ERROR + } + + private fun mapCodeValidationResponse(walletStatus: WalletStatus): WalletValidationStatus { + return if (walletStatus.verified) WalletValidationStatus.SUCCESS else WalletValidationStatus.INVALID_INPUT + } + + private fun mapError(throwable: Throwable): WalletValidationStatus { + return when (throwable) { + is HttpException -> { + var walletValidationException = WalletValidationException("") + if (throwable.code() == 400 || throwable.code() == 429) { + throwable.response() + ?.errorBody() + ?.charStream() + ?.let { + walletValidationException = gson.fromJson(it, WalletValidationException::class.java) + } ?: logger.log(TAG, throwable.message(), throwable) + } + when { + throwable.code() == 400 -> { + when (walletValidationException.status) { + "INVALID_INPUT" -> WalletValidationStatus.INVALID_INPUT + "INVALID_CODE" -> WalletValidationStatus.INVALID_CODE + "INVALID_PHONE" -> WalletValidationStatus.INVALID_PHONE + "DOUBLE_SPENT" -> WalletValidationStatus.DOUBLE_SPENT + "REGION_NOT_SUPPORTED" -> WalletValidationStatus.REGION_NOT_SUPPORTED + "LANDLINE_NOT_SUPPORTED" -> WalletValidationStatus.LANDLINE_NOT_SUPPORTED + "EXPIRED_CODE" -> WalletValidationStatus.EXPIRED_CODE + else -> WalletValidationStatus.GENERIC_ERROR + } + } + throwable.code() == 429 -> { + when (walletValidationException.status) { + "TOO_MANY_REQUESTS" -> WalletValidationStatus.TOO_MANY_ATTEMPTS + "DOUBLE_SPENT" -> WalletValidationStatus.DOUBLE_SPENT + else -> WalletValidationStatus.GENERIC_ERROR + } + } + else -> WalletValidationStatus.GENERIC_ERROR + } + } + else -> { + if (throwable.isNoNetworkException()) { + WalletValidationStatus.NO_NETWORK + } else { + WalletValidationStatus.GENERIC_ERROR + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/SmsValidationRepositoryType.kt b/app/src/main/java/com/asfoundation/wallet/repository/SmsValidationRepositoryType.kt new file mode 100644 index 00000000000..6e933fae923 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/SmsValidationRepositoryType.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import io.reactivex.Single + +interface SmsValidationRepositoryType { + + fun isValid(walletAddress: String): Single + + fun requestValidationCode(phoneNumber: String): Single + + fun validateCode(phoneNumber: String, walletAddress: String, + validationCode: String): Single + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TokenLocalSource.java b/app/src/main/java/com/asfoundation/wallet/repository/TokenLocalSource.java deleted file mode 100644 index 8eb7e30d7bb..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/TokenLocalSource.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.asfoundation.wallet.repository; - -import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.TokenTicker; -import com.asfoundation.wallet.entity.Wallet; -import io.reactivex.Completable; -import io.reactivex.Single; - -public interface TokenLocalSource { - Completable saveTokens(NetworkInfo networkInfo, Wallet wallet, Token[] items); - - void updateTokenBalance(NetworkInfo network, Wallet wallet, Token token); - - void setEnable(NetworkInfo network, Wallet wallet, Token token, boolean isEnabled); - - Single fetchEnabledTokens(NetworkInfo networkInfo, Wallet wallet); - - Single fetchAllTokens(NetworkInfo networkInfo, Wallet wallet); - - Completable saveTickers(NetworkInfo network, Wallet wallet, TokenTicker[] tokenTickers); - - Single fetchTickers(NetworkInfo network, Wallet wallet, Token[] tokens); - - Completable delete(NetworkInfo network, Wallet wallet, Token token); -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TokenRepository.java b/app/src/main/java/com/asfoundation/wallet/repository/TokenRepository.java index c4fc8e818b5..53413891707 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/TokenRepository.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/TokenRepository.java @@ -1,81 +1,33 @@ package com.asfoundation.wallet.repository; -import android.support.annotation.NonNull; -import android.text.format.DateUtils; -import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.RawTransaction; import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.TokenInfo; -import com.asfoundation.wallet.entity.TokenTicker; -import com.asfoundation.wallet.entity.TransactionOperation; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.service.TickerService; -import com.asfoundation.wallet.service.TokenExplorerClientType; -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.ObservableTransformer; +import com.asfoundation.wallet.interact.DefaultTokenProvider; import io.reactivex.Single; -import io.reactivex.SingleTransformer; import java.math.BigDecimal; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; import java.util.List; -import java.util.Set; -import okhttp3.OkHttpClient; +import org.jetbrains.annotations.NotNull; import org.web3j.abi.FunctionEncoder; -import org.web3j.abi.FunctionReturnDecoder; import org.web3j.abi.TypeReference; import org.web3j.abi.datatypes.Address; import org.web3j.abi.datatypes.Bool; import org.web3j.abi.datatypes.Function; import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.Utf8String; +import org.web3j.abi.datatypes.generated.Bytes2; import org.web3j.abi.datatypes.generated.Uint256; -import org.web3j.protocol.Web3j; -import org.web3j.protocol.Web3jFactory; -import org.web3j.protocol.core.DefaultBlockParameterName; -import org.web3j.protocol.core.methods.response.EthCall; -import org.web3j.protocol.http.HttpService; import org.web3j.utils.Numeric; -import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction; - public class TokenRepository implements TokenRepositoryType { - private static final long BALANCE_UPDATE_INTERVAL = DateUtils.MINUTE_IN_MILLIS; - private final TokenExplorerClientType tokenNetworkService; - private final WalletRepositoryType walletRepository; - private final TokenLocalSource localSource; - private final OkHttpClient httpClient; - private final EthereumNetworkRepositoryType ethereumNetworkRepository; - private final TransactionLocalSource transactionsLocalCache; - private final TickerService tickerService; - private Web3j web3j; - - public TokenRepository(OkHttpClient okHttpClient, - EthereumNetworkRepositoryType ethereumNetworkRepository, - WalletRepositoryType walletRepository, TokenExplorerClientType tokenNetworkService, - TokenLocalSource localSource, TransactionLocalSource transactionsLocalCache, - TickerService tickerService) { - this.httpClient = okHttpClient; - this.ethereumNetworkRepository = ethereumNetworkRepository; - this.walletRepository = walletRepository; - this.tokenNetworkService = tokenNetworkService; - this.localSource = localSource; - this.transactionsLocalCache = transactionsLocalCache; - this.tickerService = tickerService; - this.ethereumNetworkRepository.addOnChangeDefaultNetwork(this::buildWeb3jClient); - buildWeb3jClient(ethereumNetworkRepository.getDefaultNetwork()); - } + private final DefaultTokenProvider defaultTokenProvider; + private final WalletRepositoryType walletRepositoryType; - private static Function balanceOf(String owner) { - return new Function("balanceOf", Collections.singletonList(new Address(owner)), - Collections.singletonList(new TypeReference() { - })); + public TokenRepository(DefaultTokenProvider defaultTokenProvider, + WalletRepositoryType walletRepositoryType) { + this.defaultTokenProvider = defaultTokenProvider; + this.walletRepositoryType = walletRepositoryType; } public static byte[] createTokenTransferData(String to, BigDecimal tokenAmount) { @@ -96,17 +48,18 @@ public static byte[] createTokenApproveData(String spender, BigDecimal amount) { return Numeric.hexStringToByteArray(Numeric.cleanHexPrefix(encodedFunction)); } - public static byte[] buyData(String developerAddress, String storeAddress, String oemAddress, - String data, BigDecimal amount, String tokenAddress) { + static byte[] buyData(String developerAddress, String storeAddress, String oemAddress, + String data, BigDecimal amount, String tokenAddress, String packageName, byte[] countryCode) { Uint256 amountParam = new Uint256(amount.toBigInteger()); - Utf8String dataParam = new Utf8String(data); + Utf8String packageNameType = new Utf8String(packageName); + Utf8String dataParam = data == null ? new Utf8String("") : new Utf8String(data); Address contractAddress = new Address(tokenAddress); Address developerAddressParam = new Address(developerAddress); Address storeAddressParam = new Address(storeAddress); Address oemAddressParam = new Address(oemAddress); - List params = - Arrays.asList(amountParam, dataParam, contractAddress, developerAddressParam, - storeAddressParam, oemAddressParam); + Bytes2 countryCodeBytes = new Bytes2(countryCode); + List params = Arrays.asList(packageNameType, dataParam, amountParam, contractAddress, + developerAddressParam, storeAddressParam, oemAddressParam, countryCodeBytes); List> returnTypes = Collections.singletonList(new TypeReference() { }); Function function = new Function("buy", params, returnTypes); @@ -114,234 +67,9 @@ public static byte[] buyData(String developerAddress, String storeAddress, Strin return Numeric.hexStringToByteArray(Numeric.cleanHexPrefix(encodedFunction)); } - private void buildWeb3jClient(NetworkInfo defaultNetwork) { - web3j = Web3jFactory.build(new HttpService(defaultNetwork.rpcServerUrl, httpClient, false)); - } - - @Override public Observable fetchActive(String walletAddress) { - NetworkInfo network = ethereumNetworkRepository.getDefaultNetwork(); - Wallet wallet = new Wallet(walletAddress); - return Single.merge(fetchCachedEnabledTokens(network, wallet), // Immediately show the cache. - updateTokens(network, wallet) // Looking for new tokens - .andThen(fetchCachedEnabledTokens(network, wallet))) // and showing the cach - .map(this::removeDuplicates) - .toObservable(); - } - - @Override public Observable fetchAll(String walletAddress) { - NetworkInfo network = ethereumNetworkRepository.getDefaultNetwork(); - Wallet wallet = new Wallet(walletAddress); - return localSource.fetchAllTokens(network, wallet) - .toObservable(); - } - - @Override public Completable addToken(Wallet wallet, String address, String symbol, int decimals, - boolean isAddedManually) { - return localSource.saveTokens(ethereumNetworkRepository.getDefaultNetwork(), wallet, - new Token[] { - new Token( - new TokenInfo(address, "", symbol.toLowerCase(), decimals, true, isAddedManually), - null, 0) - }); - } - - @Override public Completable setEnable(Wallet wallet, Token token, boolean isEnabled) { - NetworkInfo network = ethereumNetworkRepository.getDefaultNetwork(); - return Completable.fromAction(() -> localSource.setEnable(network, wallet, token, isEnabled)); - } - - @Override public Completable delete(Wallet wallet, Token token) { - NetworkInfo network = ethereumNetworkRepository.getDefaultNetwork(); - return localSource.delete(network, wallet, token); - } - - private Token[] removeDuplicates(Token[] tokens) { - List toKeep = new LinkedList<>(Arrays.asList(tokens)); - - Iterator iterator = toKeep.iterator(); - - while (iterator.hasNext()) { - Token token = iterator.next(); - for (Token tmp : toKeep) { - if (tmp != token && tmp.tokenInfo.address.toLowerCase() - .equals(token.tokenInfo.address.toLowerCase())) { - if (tmp.tokenInfo.name.equals("")) { - toKeep.remove(tmp); - } else { - iterator.remove(); - } - break; - } - } - } - - return mapToArray(toKeep); - } - - private Token[] mapToArray(List toKeep) { - Token[] tokens = new Token[toKeep.size()]; - - for (int i = 0; i < tokens.length; i++) { - tokens[i] = toKeep.get(i); - } - - return tokens; - } - - private SingleTransformer attachTicker(NetworkInfo network, Wallet wallet) { - return upstream -> upstream.flatMap( - tokens -> Single.zip(Single.just(tokens), getTickers(network, wallet, tokens), - (data, tokenTickers) -> { - for (Token token : data) { - for (TokenTicker ticker : tokenTickers) { - if (token.tokenInfo.address.equalsIgnoreCase(ticker.contract)) { - token.ticker = ticker; - } - } - } - return data; - })); - } - - private Single getTickers(NetworkInfo network, Wallet wallet, Token[] tokens) { - return localSource.fetchTickers(network, wallet, tokens) - .onErrorResumeNext(throwable -> tickerService.fetchTockenTickers(tokens, "USD") - .onErrorResumeNext(thr -> Single.just(new TokenTicker[0]))) - .flatMapCompletable(tokenTickers -> localSource.saveTickers(network, wallet, tokenTickers)) - .andThen(localSource.fetchTickers(network, wallet, tokens) - .onErrorResumeNext(thr -> Single.just(new TokenTicker[0]))); - } - - private Single fetchFromNetworkSource(@NonNull NetworkInfo network, - @NonNull Wallet wallet) { - return Single.fromCallable(() -> { - try { - return network.isMainNetwork ? tokenNetworkService.fetch(wallet.address) - .blockingFirst() : new TokenInfo[0]; - } catch (Throwable th) { - // Ignore all errors, it's not important source. - return new TokenInfo[0]; - } - }) - .map(this::mapToTokens); - } - - private Single extractFromTransactions(NetworkInfo network, Wallet wallet) { - return transactionsLocalCache.fetchTransaction(network, wallet) - .flatMap(transactions -> { - List result = new ArrayList<>(); - for (RawTransaction transaction : transactions) { - if (transaction.operations == null || transaction.operations.length == 0) { - continue; - } - TransactionOperation operation = transaction.operations[0]; - result.add(new Token(new TokenInfo(operation.contract.address, operation.contract.name, - operation.contract.symbol, operation.contract.decimals, true, false), null, 0)); - } - return Single.just(result.toArray(new Token[result.size()])); - }); - } - - private Completable updateTokens(NetworkInfo network, Wallet wallet) { - return Single.zip(fetchFromNetworkSource(network, wallet), - extractFromTransactions(network, wallet), localSource.fetchAllTokens(network, wallet), - (fromNetTokens, fromTrxTokens, cachedTokens) -> { - final Set oldTokensIndex = new HashSet<>(); - final List zip = new ArrayList<>(); - zip.addAll(Arrays.asList(fromNetTokens)); - zip.addAll(Arrays.asList(fromTrxTokens)); - final List newTokens = new ArrayList<>(); - for (Token cachedToken : cachedTokens) { - oldTokensIndex.add(cachedToken.tokenInfo.address); - } - for (int i = zip.size() - 1; i > -1; i--) { - if (!oldTokensIndex.contains(zip.get(i).tokenInfo.address)) { - newTokens.add(zip.get(i)); - } - } - return newTokens.toArray(new Token[newTokens.size()]); - }) - .flatMapCompletable(tokens -> localSource.saveTokens(network, wallet, tokens)); - } - - private ObservableTransformer updateBalance(NetworkInfo network, Wallet wallet) { - return upstream -> upstream.map(token -> { - long now = System.currentTimeMillis(); - long minUpdateBalanceTime = now - BALANCE_UPDATE_INTERVAL; - if (token.balance == null || token.updateBlancaTime < minUpdateBalanceTime) { - try { - token = new Token(token.tokenInfo, getBalance(wallet, token.tokenInfo), now); - localSource.updateTokenBalance(network, wallet, token); - } catch (Throwable th) { /* Quietly */ } - } - return token; - }); - } - - private SingleTransformer attachEthereum(NetworkInfo network, Wallet wallet) { - return upstream -> Single.zip(upstream, attachEth(network, wallet), (tokens, ethToken) -> { - List result = new ArrayList<>(); - result.add(ethToken); - result.addAll(Arrays.asList(tokens)); - return result.toArray(new Token[result.size()]); - }); - } - - private Single fetchCachedEnabledTokens(NetworkInfo network, Wallet wallet) { - return localSource.fetchEnabledTokens(network, wallet) - .flatMapObservable(Observable::fromArray) - .compose(updateBalance(network, wallet)) - .toList() - .map(list -> list.toArray(new Token[list.size()])) - .compose(attachTicker(network, wallet)) - .compose(attachEthereum(network, wallet)); - } - - private Single attachEth(NetworkInfo network, Wallet wallet) { - return walletRepository.balanceInWei(wallet) - .map(balance -> { - TokenInfo info = - new TokenInfo(wallet.address, network.name, network.symbol, 18, true, false); - return new Token(info, balance, System.currentTimeMillis()); - }) - .flatMap(token -> ethereumNetworkRepository.getTicker() - .map(ticker -> { - token.ticker = new TokenTicker("", "", ticker.price, ticker.percentChange24h, null); - return token; - }) - .onErrorResumeNext(throwable -> Single.just(token))); - } - - private BigDecimal getBalance(Wallet wallet, TokenInfo tokenInfo) throws Exception { - Function function = balanceOf(wallet.address); - String responseValue = callSmartContractFunction(function, tokenInfo.address, wallet.address); - - List response = - FunctionReturnDecoder.decode(responseValue, function.getOutputParameters()); - if (response.size() == 1) { - return new BigDecimal(((Uint256) response.get(0)).getValue()); - } else { - return null; - } - } - - private String callSmartContractFunction(Function function, String contractAddress, - String walletAddress) throws Exception { - String encodedFunction = FunctionEncoder.encode(function); - org.web3j.protocol.core.methods.request.Transaction transaction = - createEthCallTransaction(walletAddress, contractAddress, encodedFunction); - EthCall response = web3j.ethCall(transaction, DefaultBlockParameterName.LATEST) - .send(); - - return response.getValue(); - } - - private Token[] mapToTokens(TokenInfo[] items) { - int len = items.length; - Token[] tokens = new Token[len]; - for (int i = 0; i < len; i++) { - tokens[i] = new Token(items[i], null, 0); - } - return tokens; + @NotNull @Override public Single getAppcBalance(@NotNull String address) { + return defaultTokenProvider.getDefaultToken() + .flatMap(tokenInfo -> walletRepositoryType.getAppcBalanceInWei(address) + .map(appcBalance -> new Token(tokenInfo, appcBalance))); } } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TokenRepositoryType.java b/app/src/main/java/com/asfoundation/wallet/repository/TokenRepositoryType.java deleted file mode 100644 index 19dd91af864..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/TokenRepositoryType.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.asfoundation.wallet.repository; - -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.Wallet; -import io.reactivex.Completable; -import io.reactivex.Observable; - -public interface TokenRepositoryType { - - Observable fetchActive(String walletAddress); - - Observable fetchAll(String walletAddress); - - Completable addToken(Wallet wallet, String address, String symbol, int decimals, - boolean isAddedManually); - - Completable setEnable(Wallet wallet, Token token, boolean isEnabled); - - Completable delete(Wallet wallet, Token token); -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TokenRepositoryType.kt b/app/src/main/java/com/asfoundation/wallet/repository/TokenRepositoryType.kt new file mode 100644 index 00000000000..d95b6018246 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/TokenRepositoryType.kt @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.entity.Token +import io.reactivex.Single + +interface TokenRepositoryType { + fun getAppcBalance(address: String): Single +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TokensRealmSource.java b/app/src/main/java/com/asfoundation/wallet/repository/TokensRealmSource.java deleted file mode 100644 index c95f0a49cff..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/TokensRealmSource.java +++ /dev/null @@ -1,264 +0,0 @@ -package com.asfoundation.wallet.repository; - -import android.text.TextUtils; -import android.text.format.DateUtils; -import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.TokenInfo; -import com.asfoundation.wallet.entity.TokenTicker; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.entity.RealmToken; -import com.asfoundation.wallet.repository.entity.RealmTokenTicker; -import com.asfoundation.wallet.service.RealmManager; -import io.reactivex.Completable; -import io.reactivex.Single; -import io.realm.Realm; -import io.realm.RealmResults; -import io.realm.Sort; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Date; - -public class TokensRealmSource implements TokenLocalSource { - - private static final long ACTUAL_BALANCE_INTERVAL = 5 * DateUtils.MINUTE_IN_MILLIS; - private static final long ACTUAL_TOKEN_TICKER_INTERVAL = 5 * DateUtils.MINUTE_IN_MILLIS; - private static final String COINMARKETCAP_IMAGE_URL = - "https://files.coinmarketcap.com/static/img/coins/128x128/%s.png"; - - private final RealmManager realmManager; - - public TokensRealmSource(RealmManager realmManager) { - this.realmManager = realmManager; - } - - @Override public Completable saveTokens(NetworkInfo networkInfo, Wallet wallet, Token[] items) { - return Completable.fromAction(() -> { - Date now = new Date(); - for (Token token : items) { - saveToken(networkInfo, wallet, token, now); - } - }); - } - - @Override public void updateTokenBalance(NetworkInfo network, Wallet wallet, Token token) { - Realm realm = null; - try { - realm = realmManager.getRealmInstance(network, wallet); - RealmToken realmToken = realm.where(RealmToken.class) - .equalTo("address", token.tokenInfo.address) - .findFirst(); - realm.beginTransaction(); - if (realmToken != null) { - realmToken.setBalance(token.balance.toString()); - } - realm.commitTransaction(); - } catch (Exception ex) { - if (realm != null) { - realm.cancelTransaction(); - } - } finally { - if (realm != null) { - realm.close(); - } - } - } - - @Override - public void setEnable(NetworkInfo network, Wallet wallet, Token token, boolean isEnabled) { - Realm realm = null; - try { - realm = realmManager.getRealmInstance(network, wallet); - RealmToken realmToken = realm.where(RealmToken.class) - .equalTo("address", token.tokenInfo.address) - .findFirst(); - realm.beginTransaction(); - if (realmToken != null) { - realmToken.setEnabled(isEnabled); - } - realm.commitTransaction(); - } catch (Exception ex) { - if (realm != null) { - realm.cancelTransaction(); - } - } finally { - if (realm != null) { - realm.close(); - } - } - } - - @Override public Single fetchEnabledTokens(NetworkInfo networkInfo, Wallet wallet) { - return Single.fromCallable(() -> { - Realm realm = null; - try { - realm = realmManager.getRealmInstance(networkInfo, wallet); - RealmResults realmItems = realm.where(RealmToken.class) - .sort("addedTime", Sort.ASCENDING) - .equalTo("isEnabled", true) - .findAll(); - return convert(realmItems, System.currentTimeMillis()); - } finally { - if (realm != null) { - realm.close(); - } - } - }); - } - - @Override public Single fetchAllTokens(NetworkInfo networkInfo, Wallet wallet) { - return Single.fromCallable(() -> { - Realm realm = null; - try { - realm = realmManager.getRealmInstance(networkInfo, wallet); - RealmResults realmItems = realm.where(RealmToken.class) - .sort("addedTime", Sort.ASCENDING) - .findAll(); - - return convert(realmItems, System.currentTimeMillis()); - } finally { - if (realm != null) { - realm.close(); - } - } - }); - } - - @Override - public Completable saveTickers(NetworkInfo network, Wallet wallet, TokenTicker[] tokenTickers) { - return Completable.fromAction(() -> { - Realm realm = null; - try { - realm = realmManager.getRealmInstance(network, wallet); - realm.beginTransaction(); - long now = System.currentTimeMillis(); - for (TokenTicker tokenTicker : tokenTickers) { - RealmTokenTicker realmItem = realm.where(RealmTokenTicker.class) - .equalTo("contract", tokenTicker.contract) - .findFirst(); - if (realmItem == null) { - realmItem = realm.createObject(RealmTokenTicker.class, tokenTicker.contract); - realmItem.setCreatedTime(now); - } - realmItem.setId(tokenTicker.id); - realmItem.setPercentChange24h(tokenTicker.percentChange24h); - realmItem.setPrice(tokenTicker.price); - realmItem.setImage( - TextUtils.isEmpty(tokenTicker.image) ? String.format(COINMARKETCAP_IMAGE_URL, - tokenTicker.id) : tokenTicker.image); - realmItem.setUpdatedTime(now); - } - realm.commitTransaction(); - } catch (Exception ex) { - if (realm != null) { - realm.cancelTransaction(); - } - } finally { - if (realm != null) { - realm.close(); - } - } - }); - } - - @Override - public Single fetchTickers(NetworkInfo network, Wallet wallet, Token[] tokens) { - return Single.fromCallable(() -> { - ArrayList tokenTickers = new ArrayList<>(); - Realm realm = null; - try { - realm = realmManager.getRealmInstance(network, wallet); - realm.beginTransaction(); - long minCreatedTime = System.currentTimeMillis() - ACTUAL_TOKEN_TICKER_INTERVAL; - RealmResults rawItems = realm.where(RealmTokenTicker.class) - .greaterThan("updatedTime", minCreatedTime) - .findAll(); - int len = rawItems.size(); - for (int i = 0; i < len; i++) { - RealmTokenTicker rawItem = rawItems.get(i); - if (rawItem != null) { - tokenTickers.add( - new TokenTicker(rawItem.getId(), rawItem.getContract(), rawItem.getPrice(), - rawItem.getPercentChange24h(), rawItem.getImage())); - } - } - realm.commitTransaction(); - } finally { - if (realm != null) { - realm.close(); - } - } - return tokenTickers.size() == 0 ? null - : tokenTickers.toArray(new TokenTicker[tokenTickers.size()]); - }); - } - - @Override public Completable delete(NetworkInfo network, Wallet wallet, Token token) { - return Completable.fromAction(() -> { - Realm realm = null; - try { - realm = realmManager.getRealmInstance(network, wallet); - realm.beginTransaction(); - RealmToken item = realm.where(RealmToken.class) - .equalTo("address", token.tokenInfo.address) - .findFirst(); - if (item != null) { - item.deleteFromRealm(); - } - realm.commitTransaction(); - } finally { - if (realm != null) { - realm.close(); - } - } - }); - } - - private void saveToken(NetworkInfo networkInfo, Wallet wallet, Token token, Date currentTime) { - Realm realm = null; - try { - realm = realmManager.getRealmInstance(networkInfo, wallet); - RealmToken realmToken = realm.where(RealmToken.class) - .equalTo("address", token.tokenInfo.address) - .findFirst(); - realm.beginTransaction(); - if (realmToken == null) { - realmToken = realm.createObject(RealmToken.class, token.tokenInfo.address); - realmToken.setName(token.tokenInfo.name); - realmToken.setSymbol(token.tokenInfo.symbol); - realmToken.setDecimals(token.tokenInfo.decimals); - realmToken.setAddedTime(currentTime.getTime()); - realmToken.setEnabled(true); - realmToken.setAddedManually(token.tokenInfo.isAddedManually); - } - realmToken.setBalance(token.balance == null ? null : token.balance.toString()); - realm.commitTransaction(); - } catch (Exception ex) { - if (realm != null) { - realm.cancelTransaction(); - } - } finally { - if (realm != null) { - realm.close(); - } - } - } - - private Token[] convert(RealmResults realmItems, long now) { - int len = realmItems.size(); - Token[] result = new Token[len]; - for (int i = 0; i < len; i++) { - RealmToken realmItem = realmItems.get(i); - if (realmItem != null) { - TokenInfo info = - new TokenInfo(realmItem.getAddress(), realmItem.getName(), realmItem.getSymbol(), - realmItem.getDecimals(), realmItem.getEnabled(), realmItem.getAddedManually()); - BigDecimal balance = TextUtils.isEmpty(realmItem.getBalance()) - || realmItem.getUpdatedTime() + ACTUAL_BALANCE_INTERVAL < now ? null - : new BigDecimal(realmItem.getBalance()); - result[i] = new Token(info, balance, realmItem.getUpdatedTime()); - } - } - return result; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TrackPendingTransactionService.java b/app/src/main/java/com/asfoundation/wallet/repository/TrackPendingTransactionService.java new file mode 100644 index 00000000000..8e8a31c8cc3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/TrackPendingTransactionService.java @@ -0,0 +1,18 @@ +package com.asfoundation.wallet.repository; + +import com.asfoundation.wallet.entity.PendingTransaction; +import io.reactivex.Observable; + +public class TrackPendingTransactionService implements TrackTransactionService { + private final PendingTransactionService trackTransactionService; + + public TrackPendingTransactionService(PendingTransactionService trackTransactionService) { + this.trackTransactionService = trackTransactionService; + } + + @Override public Observable checkTransactionState(String hash) { + return trackTransactionService.checkTransactionState(hash) + .firstOrError() + .toObservable(); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TrackTransactionService.java b/app/src/main/java/com/asfoundation/wallet/repository/TrackTransactionService.java new file mode 100644 index 00000000000..2ca2091fc07 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/TrackTransactionService.java @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.repository; + +import com.asfoundation.wallet.entity.PendingTransaction; +import io.reactivex.Observable; + +public interface TrackTransactionService { + Observable checkTransactionState(String hash); +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TransactionLocalSource.java b/app/src/main/java/com/asfoundation/wallet/repository/TransactionLocalSource.java deleted file mode 100644 index b288053f623..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/TransactionLocalSource.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.asfoundation.wallet.repository; - -import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.RawTransaction; -import com.asfoundation.wallet.entity.Wallet; -import io.reactivex.Completable; -import io.reactivex.Single; - -public interface TransactionLocalSource { - Single fetchTransaction(NetworkInfo networkInfo, Wallet wallet); - - Completable putTransactions(NetworkInfo networkInfo, Wallet wallet, - RawTransaction[] transactions); - - Single findLast(NetworkInfo networkInfo, Wallet wallet); -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TransactionMapper.kt b/app/src/main/java/com/asfoundation/wallet/repository/TransactionMapper.kt new file mode 100644 index 00000000000..f74f659be3b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/TransactionMapper.kt @@ -0,0 +1,162 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.repository.entity.OperationEntity +import com.asfoundation.wallet.repository.entity.TransactionDetailsEntity +import com.asfoundation.wallet.repository.entity.TransactionEntity +import com.asfoundation.wallet.transactions.Operation +import com.asfoundation.wallet.transactions.Transaction +import com.asfoundation.wallet.transactions.TransactionDetails +import com.asfoundation.wallet.util.BalanceUtils +import java.math.BigDecimal + +class TransactionMapper { + + fun map(transactions: List) = transactions.map { map(it) } + + private fun map(transaction: TransactionEntity): Transaction { + return Transaction(transaction.transactionId, map(transaction.type), map(transaction.subType), + transaction.title, transaction.cardDescription, map(transaction.perk), + transaction.approveTransactionId, transaction.timeStamp, transaction.processedTime, + map(transaction.status), transaction.value, transaction.from, transaction.to, + map(transaction.details), transaction.currency, mapToOperations(transaction.operations)) + } + + private fun mapToOperations(operations: List?): List? { + return operations?.map { Operation(it.transactionId, it.from, it.to, it.fee) } + } + + private fun map(details: TransactionDetailsEntity?): TransactionDetails? { + if (details == null) { + return null + } + return TransactionDetails(details.sourceName, map(details.icon), details.description) + } + + private fun map(icon: TransactionDetailsEntity.Icon): TransactionDetails.Icon { + return TransactionDetails.Icon(map(icon.iconType), icon.uri) + } + + private fun map(type: TransactionDetailsEntity.Type): TransactionDetails.Icon.Type { + return when (type) { + TransactionDetailsEntity.Type.FILE -> TransactionDetails.Icon.Type.FILE + TransactionDetailsEntity.Type.URL -> TransactionDetails.Icon.Type.URL + } + } + + private fun map(status: TransactionEntity.TransactionStatus): Transaction.TransactionStatus { + return when (status) { + TransactionEntity.TransactionStatus.SUCCESS -> Transaction.TransactionStatus.SUCCESS + TransactionEntity.TransactionStatus.FAILED -> Transaction.TransactionStatus.FAILED + TransactionEntity.TransactionStatus.PENDING -> Transaction.TransactionStatus.PENDING + } + } + + private fun map(type: TransactionEntity.TransactionType): Transaction.TransactionType { + return when (type) { + TransactionEntity.TransactionType.STANDARD -> Transaction.TransactionType.STANDARD + TransactionEntity.TransactionType.IAP -> Transaction.TransactionType.IAP + TransactionEntity.TransactionType.ADS -> Transaction.TransactionType.ADS + TransactionEntity.TransactionType.IAP_OFFCHAIN -> Transaction.TransactionType.IAP_OFFCHAIN + TransactionEntity.TransactionType.ADS_OFFCHAIN -> Transaction.TransactionType.ADS_OFFCHAIN + TransactionEntity.TransactionType.BONUS -> Transaction.TransactionType.BONUS + TransactionEntity.TransactionType.TOP_UP -> Transaction.TransactionType.TOP_UP + TransactionEntity.TransactionType.TRANSFER_OFF_CHAIN -> Transaction.TransactionType.TRANSFER_OFF_CHAIN + TransactionEntity.TransactionType.ETHER_TRANSFER -> Transaction.TransactionType.ETHER_TRANSFER + } + } + + fun map(transaction: Transaction, relatedWallet: String): TransactionEntity { + return TransactionEntity(transaction.transactionId, relatedWallet, + transaction.approveTransactionId, map(transaction.perk), + map(transaction.type), map(transaction.subType), transaction.title, + transaction.description, transaction.timeStamp, + transaction.processedTime, map(transaction.status), transaction.value, + transaction.from, transaction.to, map(transaction.details), transaction.currency, + mapToOperationEntities(transaction.operations)) + } + + private fun mapToOperationEntities(operations: List?): List? { + return operations?.map { map(it) } + } + + private fun map(operation: Operation): OperationEntity { + return OperationEntity(operation.transactionId, operation.from, operation.to, + BalanceUtils.weiToEth(BigDecimal(operation.fee)) + .toPlainString()) + } + + private fun map(details: TransactionDetails?): TransactionDetailsEntity? { + if (details == null) { + return null + } + return TransactionDetailsEntity(map(details.icon), details.sourceName, details.description) + } + + private fun map(icon: TransactionDetails.Icon): TransactionDetailsEntity.Icon { + return TransactionDetailsEntity.Icon(map(icon.type), icon.uri) + } + + private fun map(type: TransactionDetails.Icon.Type): TransactionDetailsEntity.Type { + return when (type) { + TransactionDetails.Icon.Type.FILE -> TransactionDetailsEntity.Type.FILE + TransactionDetails.Icon.Type.URL -> TransactionDetailsEntity.Type.URL + } + } + + private fun map(status: Transaction.TransactionStatus): TransactionEntity.TransactionStatus { + return when (status) { + Transaction.TransactionStatus.SUCCESS -> TransactionEntity.TransactionStatus.SUCCESS + Transaction.TransactionStatus.FAILED -> TransactionEntity.TransactionStatus.FAILED + Transaction.TransactionStatus.PENDING -> TransactionEntity.TransactionStatus.PENDING + } + } + + private fun map(type: Transaction.TransactionType): TransactionEntity.TransactionType { + return when (type) { + Transaction.TransactionType.STANDARD -> TransactionEntity.TransactionType.STANDARD + Transaction.TransactionType.IAP -> TransactionEntity.TransactionType.IAP + Transaction.TransactionType.ADS -> TransactionEntity.TransactionType.ADS + Transaction.TransactionType.IAP_OFFCHAIN -> TransactionEntity.TransactionType.IAP_OFFCHAIN + Transaction.TransactionType.ADS_OFFCHAIN -> TransactionEntity.TransactionType.ADS_OFFCHAIN + Transaction.TransactionType.BONUS -> TransactionEntity.TransactionType.BONUS + Transaction.TransactionType.TOP_UP -> TransactionEntity.TransactionType.TOP_UP + Transaction.TransactionType.TRANSFER_OFF_CHAIN -> TransactionEntity.TransactionType.TRANSFER_OFF_CHAIN + Transaction.TransactionType.ETHER_TRANSFER -> TransactionEntity.TransactionType.ETHER_TRANSFER + } + } + + private fun map(subType: TransactionEntity.SubType?): Transaction.SubType? { + if (subType == null) return null + return when (subType) { + TransactionEntity.SubType.PERK_PROMOTION -> Transaction.SubType.PERK_PROMOTION + TransactionEntity.SubType.UNKNOWN -> Transaction.SubType.UNKNOWN + } + } + + private fun map(subType: Transaction.SubType?): TransactionEntity.SubType? { + if (subType == null) return null + return when (subType) { + Transaction.SubType.PERK_PROMOTION -> TransactionEntity.SubType.PERK_PROMOTION + Transaction.SubType.UNKNOWN -> TransactionEntity.SubType.UNKNOWN + } + } + + + private fun map(perk: TransactionEntity.Perk?): Transaction.Perk? { + if (perk == null) return null + return when (perk) { + TransactionEntity.Perk.GAMIFICATION_LEVEL_UP -> Transaction.Perk.GAMIFICATION_LEVEL_UP + TransactionEntity.Perk.PACKAGE_PERK -> Transaction.Perk.PACKAGE_PERK + TransactionEntity.Perk.UNKNOWN -> Transaction.Perk.UNKNOWN + } + } + + private fun map(perk: Transaction.Perk?): TransactionEntity.Perk? { + if (perk == null) return null + return when (perk) { + Transaction.Perk.GAMIFICATION_LEVEL_UP -> TransactionEntity.Perk.GAMIFICATION_LEVEL_UP + Transaction.Perk.PACKAGE_PERK -> TransactionEntity.Perk.PACKAGE_PERK + Transaction.Perk.UNKNOWN -> TransactionEntity.Perk.UNKNOWN + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TransactionRepository.java b/app/src/main/java/com/asfoundation/wallet/repository/TransactionRepository.java index 4bf95c154b9..9ebaa04c837 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/TransactionRepository.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/TransactionRepository.java @@ -1,157 +1,166 @@ package com.asfoundation.wallet.repository; import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.RawTransaction; import com.asfoundation.wallet.entity.TransactionBuilder; -import com.asfoundation.wallet.entity.Wallet; import com.asfoundation.wallet.interact.DefaultTokenProvider; import com.asfoundation.wallet.poa.BlockchainErrorMapper; import com.asfoundation.wallet.service.AccountKeystoreService; -import com.asfoundation.wallet.service.TransactionsNetworkClientType; +import com.asfoundation.wallet.ui.iab.raiden.MultiWalletNonceObtainer; +import ethereumj.Transaction; import io.reactivex.Flowable; -import io.reactivex.Maybe; -import io.reactivex.Observable; +import io.reactivex.Scheduler; import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; import java.math.BigDecimal; import java.math.BigInteger; import org.reactivestreams.Publisher; +import org.web3j.abi.datatypes.Address; import org.web3j.protocol.Web3j; import org.web3j.protocol.Web3jFactory; import org.web3j.protocol.core.methods.response.EthSendTransaction; import org.web3j.protocol.http.HttpService; import org.web3j.utils.Numeric; -public class TransactionRepository implements TransactionRepositoryType { +import static com.asfoundation.wallet.C.ETHEREUM_NETWORK_NAME; +import static com.asfoundation.wallet.C.ROPSTEN_NETWORK_NAME; - private final EthereumNetworkRepositoryType networkRepository; +public abstract class TransactionRepository implements TransactionRepositoryType { + + private final NetworkInfo defaultNetwork; private final AccountKeystoreService accountKeystoreService; - private final TransactionLocalSource inDiskCache; - private final TransactionsNetworkClientType blockExplorerClient; private final DefaultTokenProvider defaultTokenProvider; - private final NonceGetter nonceGetter; private final BlockchainErrorMapper errorMapper; + private final MultiWalletNonceObtainer nonceObtainer; + private final Scheduler scheduler; - public TransactionRepository(EthereumNetworkRepositoryType networkRepository, - AccountKeystoreService accountKeystoreService, TransactionLocalSource inDiskCache, - TransactionsNetworkClientType blockExplorerClient, DefaultTokenProvider defaultTokenProvider, - NonceGetter nonceGetter, BlockchainErrorMapper errorMapper) { - this.networkRepository = networkRepository; + public TransactionRepository(NetworkInfo defaultNetwork, + AccountKeystoreService accountKeystoreService, DefaultTokenProvider defaultTokenProvider, + BlockchainErrorMapper errorMapper, MultiWalletNonceObtainer nonceObtainer, + Scheduler scheduler) { + this.defaultNetwork = defaultNetwork; this.accountKeystoreService = accountKeystoreService; - this.blockExplorerClient = blockExplorerClient; - this.inDiskCache = inDiskCache; this.defaultTokenProvider = defaultTokenProvider; - this.nonceGetter = nonceGetter; this.errorMapper = errorMapper; + this.nonceObtainer = nonceObtainer; + this.scheduler = scheduler; } - @Override public Observable fetchTransaction(Wallet wallet) { - NetworkInfo networkInfo = networkRepository.getDefaultNetwork(); - return Single.merge(fetchFromCache(networkInfo, wallet), - fetchAndCacheFromNetwork(networkInfo, wallet)) - .toObservable(); + public Single createTransaction(TransactionBuilder transactionBuilder, String password) { + return createTransactionAndSend(transactionBuilder, password, transactionBuilder.data(), + transactionBuilder.shouldSendToken() ? transactionBuilder.contractAddress() + : transactionBuilder.toAddress(), transactionBuilder.shouldSendToken() ? BigDecimal.ZERO + : transactionBuilder.subunitAmount()); } - @Override public Maybe findTransaction(Wallet wallet, String transactionHash) { - return fetchTransaction(wallet).firstElement() - .flatMap(transactions -> { - for (RawTransaction transaction : transactions) { - if (transaction.hash.equals(transactionHash)) { - return Maybe.just(transaction); - } - } - return null; - }); + @Override public Single approve(TransactionBuilder transactionBuilder, String password) { + return createTransactionAndSend(transactionBuilder, password, transactionBuilder.approveData(), + transactionBuilder.contractAddress(), BigDecimal.ZERO); } - public Single createTransaction(TransactionBuilder transactionBuilder, String password) { - return nonceGetter.getNonce(transactionBuilder.fromAddress()) - .flatMap(nonce -> createTransaction(transactionBuilder, password, transactionBuilder.data(), - transactionBuilder.shouldSendToken() ? transactionBuilder.contractAddress() - : transactionBuilder.toAddress(), - transactionBuilder.shouldSendToken() ? BigDecimal.ZERO - : transactionBuilder.subunitAmount(), nonce)); + @Override public Single callIab(TransactionBuilder transaction, String password) { + return defaultTokenProvider.getDefaultToken() + .observeOn(scheduler) + .flatMap( + token -> createTransactionAndSend(transaction, password, transaction.appcoinsData(), + transaction.getIabContract(), BigDecimal.ZERO)); } - @Override public Single approve(TransactionBuilder transactionBuilder, String password, - BigInteger nonce) { - return createTransaction(transactionBuilder, password, transactionBuilder.approveData(), - transactionBuilder.contractAddress(), BigDecimal.ZERO, nonce); + @Override + public Single computeApproveTransactionHash(TransactionBuilder transactionBuilder, + String password) { + return createRawTransaction(transactionBuilder, password, transactionBuilder.approveData(), + transactionBuilder.contractAddress(), BigDecimal.ZERO, + nonceObtainer.getNonce(new Address(transactionBuilder.fromAddress()), + getChainId(transactionBuilder))).map( + signedTransaction -> Numeric.toHexString(new Transaction(signedTransaction).getHash())); } - @Override - public Single callIab(TransactionBuilder transaction, String password, BigInteger nonce) { + @Override public Single computeBuyTransactionHash(TransactionBuilder transactionBuilder, + String password) { return defaultTokenProvider.getDefaultToken() - .flatMap( - token -> createTransaction(transaction, password, transaction.buyData(token.address), - transaction.getIabContract(), BigDecimal.ZERO, nonce)); + .observeOn(scheduler) + .flatMap(tokenInfo -> createRawTransaction(transactionBuilder, password, + transactionBuilder.appcoinsData(), transactionBuilder.getIabContract(), BigDecimal.ZERO, + nonceObtainer.getNonce(new Address(transactionBuilder.fromAddress()), + getChainId(transactionBuilder)))) + .map( + signedTransaction -> Numeric.toHexString(new Transaction(signedTransaction).getHash())); + } + + private Single createTransactionAndSend(TransactionBuilder transactionBuilder, + String password, byte[] data, String toAddress, BigDecimal amount) { + final Web3j web3j = Web3jFactory.build(new HttpService(defaultNetwork.rpcServerUrl)); + return Single.fromCallable( + () -> nonceObtainer.getNonce(new Address(transactionBuilder.fromAddress()), + getChainId(transactionBuilder))) + .flatMap(nonceValue -> createRawTransaction(transactionBuilder, password, data, toAddress, + amount, nonceValue).flatMap(signedMessage -> Single.fromCallable(() -> { + EthSendTransaction raw = web3j.ethSendRawTransaction(Numeric.toHexString(signedMessage)) + .send(); + if (raw.hasError()) { + throw new TransactionException(raw.getError() + .getCode(), raw.getError() + .getMessage(), raw.getError() + .getData()); + } + return raw.getTransactionHash(); + }) + .subscribeOn(Schedulers.io())) + .doOnSuccess(hash -> nonceObtainer.consumeNonce(nonceValue, + new Address(transactionBuilder.fromAddress()), getChainId(transactionBuilder))) + .retryWhen(throwableFlowable -> throwableFlowable.flatMap( + throwable -> getPublisher(throwable, nonceValue, transactionBuilder)))) + .retryWhen(throwableFlowable -> throwableFlowable.flatMap(this::retry)); } - private Single createTransaction(TransactionBuilder transactionBuilder, String password, - byte[] data, String toAddress, BigDecimal amount, BigInteger nonce) { - final Web3j web3j = - Web3jFactory.build(new HttpService(networkRepository.getDefaultNetwork().rpcServerUrl)); + private long getChainId(TransactionBuilder transactionBuilder) { + return transactionBuilder.getChainId() == TransactionBuilder.NO_CHAIN_ID + ? defaultNetwork.chainId : transactionBuilder.getChainId(); + } + private Single createRawTransaction(TransactionBuilder transactionBuilder, + String password, byte[] data, String toAddress, BigDecimal amount, BigInteger nonce) { return Single.just(nonce) .flatMap(__ -> { if (transactionBuilder.getChainId() != TransactionBuilder.NO_CHAIN_ID - && transactionBuilder.getChainId() != networkRepository.getDefaultNetwork().chainId) { + && transactionBuilder.getChainId() != defaultNetwork.chainId) { String requestedNetwork = "unknown"; - for (NetworkInfo networkInfo : networkRepository.getAvailableNetworkList()) { - if (networkInfo.chainId == transactionBuilder.getChainId()) { - requestedNetwork = networkInfo.name; - break; - } + if (transactionBuilder.getChainId() == 1) { + requestedNetwork = ETHEREUM_NETWORK_NAME; + } else if (transactionBuilder.getChainId() == 3) { + requestedNetwork = ROPSTEN_NETWORK_NAME; } return Single.error(new WrongNetworkException( "Default network is different from the intended on transaction\nCurrent network: " - + networkRepository.getDefaultNetwork().name + + defaultNetwork.name + "\nRequested: " + requestedNetwork)); } return accountKeystoreService.signTransaction(transactionBuilder.fromAddress(), password, toAddress, amount, transactionBuilder.gasSettings().gasPrice, transactionBuilder.gasSettings().gasLimit, nonce.longValue(), data, - networkRepository.getDefaultNetwork().chainId) - .flatMap(signedMessage -> Single.fromCallable(() -> { - EthSendTransaction raw = - web3j.ethSendRawTransaction(Numeric.toHexString(signedMessage)) - .send(); - if (raw.hasError()) { - throw new TransactionException(raw.getError() - .getCode(), raw.getError() - .getMessage(), raw.getError() - .getData()); - } - return raw.getTransactionHash(); - })) - .subscribeOn(Schedulers.io()); - }) - .retryWhen(throwableFlowable -> throwableFlowable.flatMap(this::getPublisher)); + defaultNetwork.chainId); + }); } - private Publisher getPublisher(Throwable throwable) { - if (errorMapper.map(throwable) - .equals(BlockchainErrorMapper.BlockchainError.NONCE_ERROR)) { - nonceGetter.bump(); + private Publisher retry(Throwable throwable) { + if (isNonceError(throwable)) { return Flowable.just(true); } return Flowable.error(throwable); } - private Single fetchFromCache(NetworkInfo networkInfo, Wallet wallet) { - return inDiskCache.fetchTransaction(networkInfo, wallet); + private Publisher getPublisher(Throwable throwable, BigInteger nonceValue, + TransactionBuilder transactionBuilder) { + if (isNonceError(throwable)) { + nonceObtainer.consumeNonce(nonceValue, new Address(transactionBuilder.fromAddress()), + getChainId(transactionBuilder)); + } + return Flowable.error(throwable); } - private Single fetchAndCacheFromNetwork(NetworkInfo networkInfo, - Wallet wallet) { - return inDiskCache.findLast(networkInfo, wallet) - .flatMap(lastTransaction -> Single.fromObservable( - blockExplorerClient.fetchLastTransactions(wallet, lastTransaction))) - .onErrorResumeNext(throwable -> Single.fromObservable( - blockExplorerClient.fetchLastTransactions(wallet, null))) - .flatMapCompletable( - transactions -> inDiskCache.putTransactions(networkInfo, wallet, transactions)) - .andThen(inDiskCache.fetchTransaction(networkInfo, wallet)); + private boolean isNonceError(Throwable throwable) { + return errorMapper.map(throwable) + .equals(BlockchainErrorMapper.BlockchainError.NONCE_ERROR); } } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TransactionRepositoryType.java b/app/src/main/java/com/asfoundation/wallet/repository/TransactionRepositoryType.java index b0d2b1781e2..e078ac3ae00 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/TransactionRepositoryType.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/TransactionRepositoryType.java @@ -1,21 +1,27 @@ package com.asfoundation.wallet.repository; -import com.asfoundation.wallet.entity.RawTransaction; import com.asfoundation.wallet.entity.TransactionBuilder; -import com.asfoundation.wallet.entity.Wallet; -import io.reactivex.Maybe; +import com.asfoundation.wallet.transactions.Transaction; import io.reactivex.Observable; import io.reactivex.Single; -import java.math.BigInteger; +import java.util.List; public interface TransactionRepositoryType { - Observable fetchTransaction(Wallet wallet); - Maybe findTransaction(Wallet wallet, String transactionHash); + Single> fetchNewTransactions(String wallet); + + Observable> fetchTransaction(String wallet); Single createTransaction(TransactionBuilder transactionBuilder, String password); - Single approve(TransactionBuilder transactionBuilder, String password, BigInteger nonce); + Single approve(TransactionBuilder transactionBuilder, String password); + + Single callIab(TransactionBuilder transaction, String password); + + Single computeApproveTransactionHash(TransactionBuilder transactionBuilder, + String password); + + Single computeBuyTransactionHash(TransactionBuilder transactionBuilder, String password); - Single callIab(TransactionBuilder transaction, String password, BigInteger nonce); + void stop(); } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TransactionTypeConverter.kt b/app/src/main/java/com/asfoundation/wallet/repository/TransactionTypeConverter.kt new file mode 100644 index 00000000000..f85223810fc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/TransactionTypeConverter.kt @@ -0,0 +1,76 @@ +package com.asfoundation.wallet.repository + +import androidx.room.TypeConverter +import com.asfoundation.wallet.repository.entity.OperationEntity +import com.asfoundation.wallet.repository.entity.TransactionDetailsEntity +import com.asfoundation.wallet.repository.entity.TransactionEntity +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +class TransactionTypeConverter { + private val gson: Gson = Gson() + + @TypeConverter + fun convertTransactionType(type: TransactionEntity.TransactionType): String { + return type.name + } + + @TypeConverter + fun convertFromTransactionType(type: String): TransactionEntity.TransactionType { + return TransactionEntity.TransactionType.valueOf(type) + } + + @TypeConverter + fun convertSubType(type: TransactionEntity.SubType?): String? { + return type?.name + } + + @TypeConverter + fun convertFromSubType(type: String?): TransactionEntity.SubType? { + return type?.let { return TransactionEntity.SubType.valueOf(it) } + } + + @TypeConverter + fun convertPerk(perk: TransactionEntity.Perk?): String? { + return perk?.name + } + + @TypeConverter + fun convertFromPerk(perk: String?): TransactionEntity.Perk? { + return perk?.let { return TransactionEntity.Perk.valueOf(it) } + } + + @TypeConverter + fun convertTransactionStatus(type: TransactionEntity.TransactionStatus): String { + return type.name + } + + @TypeConverter + fun convertFromTransactionStatus(type: String): TransactionEntity.TransactionStatus { + return TransactionEntity.TransactionStatus.valueOf(type) + } + + @TypeConverter + fun convertTransactionDetailsEntityType(type: TransactionDetailsEntity.Type): String { + return type.name + } + + @TypeConverter + fun convertFromTransactionDetailsEntityType(type: String): TransactionDetailsEntity.Type { + return TransactionDetailsEntity.Type.valueOf(type) + } + + @TypeConverter + fun stringToPermissionsList(data: String?): List? { + return data?.let { + val listType = object : TypeToken>() { + }.type + return gson.fromJson(data, listType) + } + } + + @TypeConverter + fun permissionsListToString(permissions: List?): String? { + return permissions?.let { gson.toJson(it) } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TransactionValidator.java b/app/src/main/java/com/asfoundation/wallet/repository/TransactionValidator.java new file mode 100644 index 00000000000..df187ce484f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/TransactionValidator.java @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.repository; + +import io.reactivex.Completable; + +public interface TransactionValidator { + Completable validate(PaymentTransaction paymentTransaction); +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TransactionsDao.kt b/app/src/main/java/com/asfoundation/wallet/repository/TransactionsDao.kt new file mode 100644 index 00000000000..94d4c017eb4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/TransactionsDao.kt @@ -0,0 +1,31 @@ +package com.asfoundation.wallet.repository + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.asfoundation.wallet.repository.entity.TransactionEntity +import io.reactivex.Flowable +import io.reactivex.Maybe + +@Dao +interface TransactionsDao { + + @Query( + "select * from TransactionEntity where relatedWallet like :relatedWallet order by timeStamp") + fun getAllAsFlowable(relatedWallet: String): Flowable> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertAll(roomTransactions: List) + + @Query( + "select * from TransactionEntity where relatedWallet like :relatedWallet order by processedTime desc limit 1") + fun getNewestTransaction(relatedWallet: String): Maybe + + @Query( + "select * from TransactionEntity where relatedWallet like :relatedWallet order by processedTime asc limit 1") + fun getOlderTransaction(relatedWallet: String): Maybe + + @Query("DELETE FROM TransactionEntity") + fun deleteAllTransactions() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TransactionsDatabase.kt b/app/src/main/java/com/asfoundation/wallet/repository/TransactionsDatabase.kt new file mode 100644 index 00000000000..7924deffb16 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/TransactionsDatabase.kt @@ -0,0 +1,63 @@ +package com.asfoundation.wallet.repository + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.asfoundation.wallet.repository.entity.TransactionDetailsEntity +import com.asfoundation.wallet.repository.entity.TransactionEntity + +@Database( + entities = [TransactionEntity::class, TransactionDetailsEntity::class, TransactionDetailsEntity.Icon::class], + version = 4) +@TypeConverters(TransactionTypeConverter::class) +abstract class TransactionsDatabase : RoomDatabase() { + + abstract fun transactionsDao(): TransactionsDao + + companion object { + + val MIGRATION_1_2: Migration = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS TransactionEntityCopy (transactionId TEXT NOT " + + "NULL, relatedWallet TEXT NOT NULL, approveTransactionId TEXT, type TEXT NOT " + + "NULL, timeStamp INTEGER NOT NULL, processedTime INTEGER NOT NULL, status " + + "TEXT NOT NULL, value TEXT NOT NULL, `from` TEXT NOT NULL, `to` TEXT NOT NULL, " + + "currency TEXT, operations TEXT, sourceName TEXT, description TEXT, " + + "iconType TEXT, uri TEXT, PRIMARY KEY(transactionId, relatedWallet))") + database.execSQL("INSERT INTO TransactionEntityCopy (transactionId, relatedWallet, " + + "approveTransactionId, type, timeStamp, processedTime, status, value, `from`, `to`," + + " currency, operations, sourceName, description, iconType, uri) SELECT " + + "transactionId, relatedWallet,approveTransactionId, type, timeStamp, processedTime," + + " status, value, `from`, `to`, currency, operations, sourceName, description, " + + "iconType, uri FROM TransactionEntity") + database.execSQL("DROP TABLE TransactionEntity") + database.execSQL("ALTER TABLE TransactionEntityCopy RENAME TO TransactionEntity") + } + } + + val MIGRATION_2_3: Migration = object : Migration(2, 3) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DELETE FROM TransactionEntity") + database.execSQL("DELETE FROM TransactionDetailsEntity") + database.execSQL("DELETE FROM Icon") + } + } + + //Adds 3 new values to the object related to the perk promotions + val MIGRATION_3_4: Migration = object : Migration(3, 4) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE TransactionEntity ADD COLUMN perk TEXT") + database.execSQL("ALTER TABLE TransactionEntity ADD COLUMN subType TEXT") + database.execSQL("ALTER TABLE TransactionEntity ADD COLUMN title TEXT") + database.execSQL("ALTER TABLE TransactionEntity ADD COLUMN cardDescription TEXT") + //Delete rows created after 14th September 2020 12:00 so that every perk bonus + // transactions that happened after this is converted into the layout of the perk bonus + // processed time has milliseconds into account + database.execSQL("DELETE FROM TransactionEntity WHERE processedTime >= 1600084800000") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TransactionsLoadObservable.kt b/app/src/main/java/com/asfoundation/wallet/repository/TransactionsLoadObservable.kt new file mode 100644 index 00000000000..f2e9d019191 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/TransactionsLoadObservable.kt @@ -0,0 +1,46 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.transactions.Transaction +import io.reactivex.Observable +import io.reactivex.Observer +import io.reactivex.disposables.Disposable +import java.util.concurrent.atomic.AtomicBoolean + +class TransactionsLoadObservable(private val offChainTransactions: OffChainTransactions, + private val wallet: String, + private val startingDate: Long? = null, + private val endDate: Long? = null, + private val sort: OffChainTransactions.Sort? = null, + private val limit: Int = 10) : + Observable>() { + + override fun subscribeActual(observer: Observer>) { + val transactionDisposable = TransactionsDisposable() + observer.onSubscribe(transactionDisposable) + try { + var i = 0 + var list: List? = null + while (!transactionDisposable.isDisposed && (list == null || list.isNotEmpty())) { + list = offChainTransactions.getTransactions(wallet, startingDate, endDate, i, sort, limit) + if (!transactionDisposable.isDisposed) { + observer.onNext(list) + } + i += limit + } + observer.onComplete() + } catch (ex: Exception) { + observer.onError(ex) + } + } + + inner class TransactionsDisposable : Disposable { + private val unsubscribed = AtomicBoolean() + override fun isDisposed(): Boolean { + return unsubscribed.get() + } + + override fun dispose() { + unsubscribed.compareAndSet(false, true) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TransactionsLocalRepository.kt b/app/src/main/java/com/asfoundation/wallet/repository/TransactionsLocalRepository.kt new file mode 100644 index 00000000000..fd78c5f2ee0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/TransactionsLocalRepository.kt @@ -0,0 +1,58 @@ +package com.asfoundation.wallet.repository + +import android.content.SharedPreferences +import com.asfoundation.wallet.repository.entity.TransactionEntity +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.Single + +class TransactionsLocalRepository(private val database: TransactionsDao, + private val sharedPreferences: SharedPreferences) : + TransactionsRepository { + + companion object { + private const val OLD_TRANSACTIONS_LOAD = "IS_OLD_TRANSACTIONS_LOADED" + private const val LAST_LOCALE = "locale" + } + + + override fun getAllAsFlowable(relatedWallet: String): Flowable> { + return database.getAllAsFlowable(relatedWallet) + } + + override fun insertAll(roomTransactions: List) { + return database.insertAll(roomTransactions) + } + + override fun getNewestTransaction(relatedWallet: String): Maybe { + return database.getNewestTransaction(relatedWallet) + + } + + override fun getOlderTransaction(relatedWallet: String): Maybe { + return database.getOlderTransaction(relatedWallet) + } + + + override fun isOldTransactionsLoaded(): Single { + return Single.fromCallable { sharedPreferences.getBoolean(OLD_TRANSACTIONS_LOAD, false) } + } + + override fun deleteAllTransactions() { + return database.deleteAllTransactions() + } + + override fun oldTransactionsLoaded() { + sharedPreferences.edit() + .putBoolean(OLD_TRANSACTIONS_LOAD, true) + .apply() + } + + override fun setLocale(locale: String) { + sharedPreferences.edit() + .putString(LAST_LOCALE, locale) + .apply() + } + + override fun getLastLocale() = sharedPreferences.getString(LAST_LOCALE, null) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TransactionsRealmCache.java b/app/src/main/java/com/asfoundation/wallet/repository/TransactionsRealmCache.java deleted file mode 100644 index 63ac20edb2b..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/TransactionsRealmCache.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.asfoundation.wallet.repository; - -import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.RawTransaction; -import com.asfoundation.wallet.entity.TransactionContract; -import com.asfoundation.wallet.entity.TransactionOperation; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.entity.RealmTransaction; -import com.asfoundation.wallet.repository.entity.RealmTransactionContract; -import com.asfoundation.wallet.repository.entity.RealmTransactionOperation; -import com.asfoundation.wallet.service.RealmManager; -import io.reactivex.Completable; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; -import io.realm.Realm; -import io.realm.RealmResults; -import io.realm.Sort; - -public class TransactionsRealmCache implements TransactionLocalSource { - - private final RealmManager realmManager; - - public TransactionsRealmCache(RealmManager realmManager) { - this.realmManager = realmManager; - } - - @Override - public Single fetchTransaction(NetworkInfo networkInfo, Wallet wallet) { - return Single.fromCallable(() -> { - Realm instance = null; - try { - instance = realmManager.getRealmInstance(networkInfo, wallet); - RealmResults list = instance.where(RealmTransaction.class) - .sort("nonce", Sort.DESCENDING) - .findAll(); - return convert(list); - } finally { - if (instance != null) { - instance.close(); - } - } - }); - } - - @Override public Completable putTransactions(NetworkInfo networkInfo, Wallet wallet, - RawTransaction[] transactions) { - return Completable.fromAction(() -> { - Realm instance = null; - try { - instance = realmManager.getRealmInstance(networkInfo, wallet); - instance.beginTransaction(); - for (RawTransaction transaction : transactions) { - RealmTransaction item = instance.where(RealmTransaction.class) - .equalTo("hash", transaction.hash) - .findFirst(); - if (item == null) { - item = instance.createObject(RealmTransaction.class, transaction.hash); - } - fill(instance, item, transaction); - } - instance.commitTransaction(); - } catch (Exception ex) { - if (instance != null) { - instance.cancelTransaction(); - } - } finally { - if (instance != null) { - instance.close(); - } - } - }) - .subscribeOn(Schedulers.io()); - } - - @Override public Single findLast(NetworkInfo networkInfo, Wallet wallet) { - return Single.fromCallable(() -> { - Realm realm = null; - try { - realm = realmManager.getRealmInstance(networkInfo, wallet); - return convert(realm.where(RealmTransaction.class) - .sort("timeStamp", Sort.DESCENDING) - .findAll() - .first()); - } finally { - if (realm != null) { - realm.close(); - } - } - }) - .observeOn(Schedulers.io()); - } - - private void fill(Realm realm, RealmTransaction item, RawTransaction transaction) { - item.setError(transaction.error); - item.setBlockNumber(transaction.blockNumber); - item.setTimeStamp(transaction.timeStamp); - item.setNonce(transaction.nonce); - item.setFrom(transaction.from); - item.setTo(transaction.to); - item.setValue(transaction.value); - item.setGas(transaction.gas); - item.setGasPrice(transaction.gasPrice); - item.setInput(transaction.input); - item.setGasUsed(transaction.gasUsed); - - for (TransactionOperation operation : transaction.operations) { - RealmTransactionOperation realmOperation = - realm.createObject(RealmTransactionOperation.class); - realmOperation.setTransactionId(operation.transactionId); - realmOperation.setViewType(operation.viewType); - realmOperation.setFrom(operation.from); - realmOperation.setTo(operation.to); - realmOperation.setValue(operation.value); - - RealmTransactionContract realmContract = realm.createObject(RealmTransactionContract.class); - realmContract.setAddress(operation.contract.address); - realmContract.setName(operation.contract.name); - realmContract.setTotalSupply(operation.contract.totalSupply); - realmContract.setDecimals(operation.contract.decimals); - realmContract.setSymbol(operation.contract.symbol); - - realmOperation.setContract(realmContract); - item.getOperations() - .add(realmOperation); - } - } - - private RawTransaction[] convert(RealmResults items) { - int len = items.size(); - RawTransaction[] result = new RawTransaction[len]; - for (int i = 0; i < len; i++) { - result[i] = convert(items.get(i)); - } - return result; - } - - private RawTransaction convert(RealmTransaction rawItem) { - int len = rawItem.getOperations() - .size(); - TransactionOperation[] operations = new TransactionOperation[len]; - for (int i = 0; i < len; i++) { - RealmTransactionOperation rawOperation = rawItem.getOperations() - .get(i); - if (rawOperation == null) { - continue; - } - TransactionOperation operation = new TransactionOperation(); - operation.transactionId = rawOperation.getTransactionId(); - operation.viewType = rawOperation.getViewType(); - operation.from = rawOperation.getFrom(); - operation.to = rawOperation.getTo(); - operation.value = rawOperation.getValue(); - operation.contract = new TransactionContract(); - operation.contract.address = rawOperation.getContract() - .getAddress(); - operation.contract.name = rawOperation.getContract() - .getName(); - operation.contract.totalSupply = rawOperation.getContract() - .getTotalSupply(); - operation.contract.decimals = rawOperation.getContract() - .getDecimals(); - operation.contract.symbol = rawOperation.getContract() - .getSymbol(); - operations[i] = operation; - } - return new RawTransaction(rawItem.getHash(), rawItem.getError(), rawItem.getBlockNumber(), - rawItem.getTimeStamp(), rawItem.getNonce(), rawItem.getFrom(), rawItem.getTo(), - rawItem.getValue(), rawItem.getGas(), rawItem.getGasPrice(), rawItem.getInput(), - rawItem.getGasUsed(), operations); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TransactionsRepository.kt b/app/src/main/java/com/asfoundation/wallet/repository/TransactionsRepository.kt new file mode 100644 index 00000000000..ac839bb4ced --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/TransactionsRepository.kt @@ -0,0 +1,27 @@ +package com.asfoundation.wallet.repository + +import com.asfoundation.wallet.repository.entity.TransactionEntity +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.Single + +interface TransactionsRepository { + + fun getAllAsFlowable(relatedWallet: String): Flowable> + + fun insertAll(roomTransactions: List) + + fun getNewestTransaction(relatedWallet: String): Maybe + + fun getOlderTransaction(relatedWallet: String): Maybe + + fun isOldTransactionsLoaded(): Single + + fun oldTransactionsLoaded() + + fun deleteAllTransactions() + + fun setLocale(locale: String) + + fun getLastLocale(): String? +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TrustPasswordStore.java b/app/src/main/java/com/asfoundation/wallet/repository/TrustPasswordStore.java deleted file mode 100644 index e5bdb5234e2..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/TrustPasswordStore.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.asfoundation.wallet.repository; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Build; -import android.preference.PreferenceManager; -import android.widget.Toast; -import com.asfoundation.wallet.entity.ServiceErrorException; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.util.KS; -import com.crashlytics.android.Crashlytics; -import com.wallet.pwd.trustapp.PasswordManager; -import io.reactivex.Completable; -import io.reactivex.Single; -import java.security.SecureRandom; -import java.util.Map; - -public class TrustPasswordStore implements PasswordStore { - - private final Context context; - - public TrustPasswordStore(Context context) { - this.context = context; - - migrate(); - } - - private void migrate() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - return; - } - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); - Map passwords = pref.getAll(); - for (String key : passwords.keySet()) { - if (key.contains("-pwd")) { - String address = key.replace("-pwd", ""); - try { - KS.put(context, address.toLowerCase(), PasswordManager.getPassword(address, context)); - } catch (Exception ex) { - Toast.makeText(context, "Could not process passwords.", Toast.LENGTH_LONG) - .show(); - ex.printStackTrace(); - } - } - } - } - - @Override public Single getPassword(Wallet wallet) { - return Single.fromCallable(() -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - try { - return new String(KS.get(context, wallet.address)); - } catch (Exception ex) { - Crashlytics.logException(ex); - throw new ServiceErrorException(ServiceErrorException.KEY_STORE_ERROR, - "Failed to get the password from the store."); - } - } else { - try { - return PasswordManager.getPassword(wallet.address, context); - } catch (Exception ex) { - Crashlytics.logException(ex); - throw new ServiceErrorException(ServiceErrorException.KEY_STORE_ERROR, - "Failed to get the password from the password manager."); - } - } - }); - } - - @Override public Completable setPassword(Wallet wallet, String password) { - return Completable.fromAction(() -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - KS.put(context, wallet.address, password); - } else { - try { - PasswordManager.setPassword(wallet.address, password, context); - } catch (Exception e) { - throw new ServiceErrorException(ServiceErrorException.KEY_STORE_ERROR); - } - } - }); - } - - @Override public Single generatePassword() { - return Single.fromCallable(() -> { - byte bytes[] = new byte[256]; - SecureRandom random = new SecureRandom(); - random.nextBytes(bytes); - return new String(bytes); - }); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/TrustPasswordStore.kt b/app/src/main/java/com/asfoundation/wallet/repository/TrustPasswordStore.kt new file mode 100644 index 00000000000..01256ce2404 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/TrustPasswordStore.kt @@ -0,0 +1,159 @@ +package com.asfoundation.wallet.repository + +import android.content.Context +import android.os.Build +import android.preference.PreferenceManager +import android.widget.Toast +import com.asfoundation.wallet.entity.ServiceErrorException +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.util.KS +import com.asfoundation.wallet.util.KS.ANDROID_KEY_STORE +import com.wallet.pwd.trustapp.PasswordManager +import io.reactivex.Completable +import io.reactivex.Single +import java.io.IOException +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom +import java.security.cert.CertificateException +import java.util.* + + +class TrustPasswordStore(private val context: Context, + private val logger: Logger) : + PasswordStore { + companion object { + private val TAG = TrustPasswordStore::class.java.simpleName + private const val DEFAULT_WALLET = "0x123456789" + } + + init { + migrate() + } + + private fun migrate() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return + } + val pref = + PreferenceManager.getDefaultSharedPreferences(context) + val passwords = pref.all + for (key in passwords.keys) { + if (key.contains("-pwd")) { + val address = key.replace("-pwd", "") + try { + KS.put(context, address.toLowerCase(), PasswordManager.getPassword(address, context)) + } catch (ex: Exception) { + Toast.makeText(context, "Could not process passwords.", Toast.LENGTH_LONG) + .show() + ex.printStackTrace() + } + } + } + } + + override fun getPassword(address: String): Single { + return Single.fromCallable { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return@fromCallable String(KS.get(context, address)) + } else { + return@fromCallable PasswordManager.getPassword(address, context) + } + } + .onErrorResumeNext { throwable: Throwable? -> + logError(throwable) + getPasswordFallBack(address) + } + } + + override fun setPassword(address: String, password: String): Completable { + return Completable.fromAction { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + KS.put(context, address, password) + } else { + try { + PasswordManager.setPassword(address, password, context) + } catch (e: Exception) { + val exception = ServiceErrorException(ServiceErrorException.KEY_STORE_ERROR) + logger.log(TAG, exception) + throw exception + } + } + } + } + + override fun generatePassword(): Single { + return Single.fromCallable { + val bytes = ByteArray(256) + val random = SecureRandom() + random.nextBytes(bytes) + String(bytes) + } + } + + override fun setBackUpPassword(masterPassword: String): Completable { + return setPassword(DEFAULT_WALLET, masterPassword) + } + + private fun getPasswordFallBack(walletAddress: String): Single { + return Single.fromCallable { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + return@fromCallable String(KS.get(context, DEFAULT_WALLET)) + } catch (ex: Exception) { + logger.log(TAG, ex.message, ex) + val exception = ServiceErrorException(ServiceErrorException.KEY_STORE_ERROR, + "Failed to get the password from the store.") + logError(exception) + throw exception + } + } else { + try { + return@fromCallable PasswordManager.getPassword(DEFAULT_WALLET, context) + } catch (ex: Exception) { + logger.log(TAG, ex.message, ex) + val exception = ServiceErrorException(ServiceErrorException.KEY_STORE_ERROR, + "Failed to get the password from the password manager.") + logError(exception) + throw exception + } + } + } + .flatMap { password: String -> + setPassword(walletAddress, password).andThen( + Single.just(password)) + } + } + + private fun logError(t: Throwable?) { + logger.log(TAG, t?.message, t) + try { + val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE) + keyStore.load(null) + val strBuilder = StringBuilder() + strBuilder.append("List of alias available in the keystore: ") + val enumeration: Enumeration = keyStore.aliases() + while (enumeration.hasMoreElements()) { + val alias: String = enumeration.nextElement() + strBuilder.append("[$alias] ") + } + logger.log(TAG, strBuilder.toString()) + } catch (e: Exception) { + when (e) { + is KeyStoreException -> { + logger.log(TAG, "Failed to get Android keystore or aliases from keystore", e) + } + is CertificateException, + is IOException, + is NoSuchAlgorithmException -> { + logger.log(TAG, "Failed to load keystore", e) + } + else -> { + logger.log(TAG, "Failed for unknown reason", e) + throw e + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/WalletRepository.java b/app/src/main/java/com/asfoundation/wallet/repository/WalletRepository.java index 36f1a4177cf..76b168c3d31 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/WalletRepository.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/WalletRepository.java @@ -1,31 +1,35 @@ package com.asfoundation.wallet.repository; +import com.asfoundation.wallet.analytics.AmplitudeAnalytics; +import com.asfoundation.wallet.analytics.AnalyticsSetup; import com.asfoundation.wallet.entity.Wallet; import com.asfoundation.wallet.service.AccountKeystoreService; +import com.asfoundation.wallet.service.WalletBalanceService; import io.reactivex.Completable; +import io.reactivex.Scheduler; import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; import java.math.BigDecimal; -import okhttp3.OkHttpClient; -import org.web3j.protocol.Web3jFactory; -import org.web3j.protocol.core.DefaultBlockParameterName; -import org.web3j.protocol.http.HttpService; +import org.jetbrains.annotations.NotNull; public class WalletRepository implements WalletRepositoryType { - private final PreferenceRepositoryType preferenceRepositoryType; + private final PreferencesRepositoryType preferencesRepositoryType; private final AccountKeystoreService accountKeystoreService; - private final EthereumNetworkRepositoryType networkRepository; - private final OkHttpClient httpClient; + private final WalletBalanceService walletBalanceService; + private final Scheduler networkScheduler; + private final AnalyticsSetup analyticsSetUp; + private final AmplitudeAnalytics amplitudeAnalytics; - public WalletRepository(OkHttpClient okHttpClient, - PreferenceRepositoryType preferenceRepositoryType, - AccountKeystoreService accountKeystoreService, - EthereumNetworkRepositoryType networkRepository) { - this.httpClient = okHttpClient; - this.preferenceRepositoryType = preferenceRepositoryType; + public WalletRepository(PreferencesRepositoryType preferencesRepositoryType, + AccountKeystoreService accountKeystoreService, WalletBalanceService walletBalanceService, + Scheduler networkScheduler, AnalyticsSetup analyticsSetUp, + AmplitudeAnalytics amplitudeAnalytics) { + this.preferencesRepositoryType = preferencesRepositoryType; this.accountKeystoreService = accountKeystoreService; - this.networkRepository = networkRepository; + this.walletBalanceService = walletBalanceService; + this.networkScheduler = networkScheduler; + this.analyticsSetUp = analyticsSetUp; + this.amplitudeAnalytics = amplitudeAnalytics; } @Override public Single fetchWallets() { @@ -48,30 +52,34 @@ public WalletRepository(OkHttpClient okHttpClient, } @Override - public Single importKeystoreToWallet(String store, String password, String newPassword) { - return accountKeystoreService.importKeystore(store, password, newPassword); + public Single restoreKeystoreToWallet(String store, String password, String newPassword) { + return accountKeystoreService.restoreKeystore(store, password, newPassword); } - @Override public Single importPrivateKeyToWallet(String privateKey, String newPassword) { - return accountKeystoreService.importPrivateKey(privateKey, newPassword); + @Override public Single restorePrivateKeyToWallet(String privateKey, String newPassword) { + return accountKeystoreService.restorePrivateKey(privateKey, newPassword); } - @Override public Single exportWallet(Wallet wallet, String password, String newPassword) { - return accountKeystoreService.exportAccount(wallet, password, newPassword); + @Override + public Single exportWallet(String address, String password, String newPassword) { + return accountKeystoreService.exportAccount(address, password, newPassword); } @Override public Completable deleteWallet(String address, String password) { return accountKeystoreService.deleteAccount(address, password); } - @Override public Completable setDefaultWallet(Wallet wallet) { - return Completable.fromAction( - () -> preferenceRepositoryType.setCurrentWalletAddress(wallet.address)); + @Override public Completable setDefaultWallet(String address) { + return Completable.fromAction(() -> { + analyticsSetUp.setUserId(address); + amplitudeAnalytics.setUserId(address); + preferencesRepositoryType.setCurrentWalletAddress(address); + }); } @Override public Single getDefaultWallet() { return Single.fromCallable(() -> { - String currentWalletAddress = preferenceRepositoryType.getCurrentWalletAddress(); + String currentWalletAddress = preferencesRepositoryType.getCurrentWalletAddress(); if (currentWalletAddress == null) { throw new WalletNotFoundException(); } else { @@ -81,12 +89,15 @@ public Single importKeystoreToWallet(String store, String password, Stri .flatMap(this::findWallet); } - @Override public Single balanceInWei(Wallet wallet) { - return Single.fromCallable(() -> new BigDecimal(Web3jFactory.build( - new HttpService(networkRepository.getDefaultNetwork().rpcServerUrl, httpClient, false)) - .ethGetBalance(wallet.address, DefaultBlockParameterName.LATEST) - .send() - .getBalance())) - .subscribeOn(Schedulers.io()); + @NotNull @Override public Single getEthBalanceInWei(String address) { + return walletBalanceService.getWalletBalance(address) + .map(walletBalance -> new BigDecimal(walletBalance.getEth())) + .subscribeOn(networkScheduler); + } + + @NotNull @Override public Single getAppcBalanceInWei(String address) { + return walletBalanceService.getWalletBalance(address) + .map(walletBalance -> new BigDecimal(walletBalance.getAppc())) + .subscribeOn(networkScheduler); } } \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/repository/WalletRepositoryType.java b/app/src/main/java/com/asfoundation/wallet/repository/WalletRepositoryType.java index fa2893044a9..c36c0212448 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/WalletRepositoryType.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/WalletRepositoryType.java @@ -12,17 +12,19 @@ public interface WalletRepositoryType { Single createWallet(String password); - Single importKeystoreToWallet(String store, String password, String newPassword); + Single restoreKeystoreToWallet(String store, String password, String newPassword); - Single importPrivateKeyToWallet(String privateKey, String newPassword); + Single restorePrivateKeyToWallet(String privateKey, String newPassword); - Single exportWallet(Wallet wallet, String password, String newPassword); + Single exportWallet(String address, String password, String newPassword); Completable deleteWallet(String address, String password); - Completable setDefaultWallet(Wallet wallet); + Completable setDefaultWallet(String address); Single getDefaultWallet(); - Single balanceInWei(Wallet wallet); + Single getEthBalanceInWei(String address); + + Single getAppcBalanceInWei(String address); } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/WatchedTransactionService.kt b/app/src/main/java/com/asfoundation/wallet/repository/WatchedTransactionService.kt new file mode 100644 index 00000000000..0f16735edfc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/WatchedTransactionService.kt @@ -0,0 +1,101 @@ +package com.asfoundation.wallet.repository + +import com.appcoins.wallet.commons.Repository +import com.asfoundation.wallet.entity.TransactionBuilder +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import java.util.concurrent.TimeUnit + +class WatchedTransactionService(private val transactionSender: TransactionSender, + private val cache: Repository, + private val errorMapper: ErrorMapper, + private val scheduler: Scheduler, + private val transactionTracker: TrackTransactionService) { + + fun start() { + cache.all + .observeOn(scheduler) + .flatMapCompletable { paymentTransactions -> + Observable.fromIterable(paymentTransactions) + .filter { transaction -> transaction.status == Transaction.Status.PENDING } + .flatMapCompletable { executeTransaction(it) } + } + .doOnError { it.printStackTrace() } + .retry() + .subscribe() + } + + private fun executeTransaction(transaction: Transaction): Completable { + return cache.save(transaction.key, + Transaction(transaction.key, Transaction.Status.PROCESSING, transaction.transactionBuilder)) + .observeOn(scheduler) + .andThen(transactionSender.send(transaction.transactionBuilder) + .flatMapCompletable { hash -> + cache.save(transaction.key, + Transaction(transaction.key, Transaction.Status.PROCESSING, + transaction.transactionBuilder, hash)) + .andThen(transactionTracker.checkTransactionState(hash) + .retryWhen { retryOnTransactionNotFound(it) } + .ignoreElements() + .andThen(cache.save(transaction.key, + Transaction(transaction.key, Transaction.Status.COMPLETED, + transaction.transactionBuilder, hash)))) + }) + .doOnError { + it.printStackTrace() + cache.saveSync(transaction.key, + Transaction(transaction.key, enumValueOf(errorMapper.map(it).paymentState.name), + transaction.transactionBuilder)) + } + } + + private fun retryOnTransactionNotFound(throwable: Observable): Observable { + return throwable.flatMap { + if (it is TransactionNotFoundException) { + Observable.timer(1, TimeUnit.SECONDS, Schedulers.trampoline()) + } else { + Observable.error(it) + } + } + } + + fun sendTransaction(key: String, transactionBuilder: TransactionBuilder): Completable { + return cache.save(key, Transaction(key, Transaction.Status.PENDING, transactionBuilder)) + } + + fun getTransaction(key: String): Observable = + cache.get(key) + .filter { it.status != Transaction.Status.PENDING } + + + fun getAll(): Observable> = + cache.all.flatMapSingle { transactions -> + Observable.fromIterable(transactions) + .filter { it.status != Transaction.Status.PENDING } + .toList() + } + + + fun remove(key: String): Completable = cache.remove(key) + +} + +data class Transaction( + val key: String, + val status: Status, + val transactionBuilder: TransactionBuilder, + val transactionHash: String? = null) { + + enum class Status { + PENDING, PROCESSING, COMPLETED, ERROR, WRONG_NETWORK, NONCE_ERROR, UNKNOWN_TOKEN, NO_TOKENS, + NO_ETHER, NO_FUNDS, NO_INTERNET, FORBIDDEN + } + +} + +interface TransactionSender { + fun send(transactionBuilder: TransactionBuilder): Single +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/Web3jProvider.java b/app/src/main/java/com/asfoundation/wallet/repository/Web3jProvider.java index 4e3fb59db57..15acdbb85bf 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/Web3jProvider.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/Web3jProvider.java @@ -13,15 +13,13 @@ public class Web3jProvider { private final OkHttpClient httpClient; - private final EthereumNetworkRepositoryType ethereumNetworkRepository; + private final NetworkInfo networkInfo; private Web3j web3j; - public Web3jProvider(EthereumNetworkRepositoryType ethereumNetworkRepository, - OkHttpClient client) { + public Web3jProvider(OkHttpClient client, NetworkInfo networkInfo) { httpClient = client; - this.ethereumNetworkRepository = ethereumNetworkRepository; - this.ethereumNetworkRepository.addOnChangeDefaultNetwork(this::buildWeb3jClient); - buildWeb3jClient(ethereumNetworkRepository.getDefaultNetwork()); + this.networkInfo = networkInfo; + buildWeb3jClient(networkInfo); } private void buildWeb3jClient(NetworkInfo defaultNetwork) { @@ -32,9 +30,7 @@ public Web3j getDefault() { return web3j; } - public Web3j get(int chainId) { - return Web3jFactory.build( - new HttpService(ethereumNetworkRepository.getNetwork(chainId).rpcServerUrl, httpClient, - false)); + public Web3j get() { + return Web3jFactory.build(new HttpService(networkInfo.rpcServerUrl, httpClient, false)); } } diff --git a/app/src/main/java/com/asfoundation/wallet/repository/Web3jService.java b/app/src/main/java/com/asfoundation/wallet/repository/Web3jService.java index 9a89739cd34..ad44c4ddb69 100644 --- a/app/src/main/java/com/asfoundation/wallet/repository/Web3jService.java +++ b/app/src/main/java/com/asfoundation/wallet/repository/Web3jService.java @@ -41,10 +41,6 @@ private Single getTransaction(String hash, Web3j web3jClient return Single.defer(() -> getTransaction(hash, web3j.getDefault())); } - @Override public Single getTransaction(String hash, int chainId) { - return Single.defer(() -> getTransaction(hash, web3j.get(chainId))); - } - private boolean isPending(EthTransaction ethTransaction) { org.web3j.protocol.core.methods.response.Transaction transaction = ethTransaction.getTransaction(); diff --git a/app/src/main/java/com/asfoundation/wallet/repository/entity/OperationEntity.kt b/app/src/main/java/com/asfoundation/wallet/repository/entity/OperationEntity.kt new file mode 100644 index 00000000000..1d7dba23a35 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/entity/OperationEntity.kt @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.repository.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class OperationEntity(@PrimaryKey var transactionId: String, + var from: String, + var to: String, + var fee: String) diff --git a/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmToken.java b/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmToken.java deleted file mode 100644 index 2cba3bdd243..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmToken.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.asfoundation.wallet.repository.entity; - -import io.realm.RealmObject; -import io.realm.annotations.PrimaryKey; - -public class RealmToken extends RealmObject { - @PrimaryKey private String address; - private String name; - private String symbol; - private int decimals; - private long addedTime; - private long updatedTime; - private String balance; - private boolean isEnabled; - private boolean isAddedManually; - - public int getDecimals() { - return decimals; - } - - public void setDecimals(int decimals) { - this.decimals = decimals; - } - - public String getSymbol() { - return symbol; - } - - public void setSymbol(String symbol) { - this.symbol = symbol; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getAddress() { - return address; - } - - public void setAddress(String address) { - this.address = address; - } - - public long getAddedTime() { - return addedTime; - } - - public void setAddedTime(long addedTime) { - this.addedTime = addedTime; - } - - public long getUpdatedTime() { - return updatedTime; - } - - public void setUpdatedTime(long updatedTime) { - this.updatedTime = updatedTime; - } - - public String getBalance() { - return balance; - } - - public void setBalance(String balance) { - this.balance = balance; - } - - public boolean getEnabled() { - return isEnabled; - } - - public void setEnabled(boolean isEnabled) { - this.isEnabled = isEnabled; - } - - public boolean getAddedManually() { - return isAddedManually; - } - - public void setAddedManually(boolean isAddedManually) { - this.isAddedManually = isAddedManually; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmTokenTicker.java b/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmTokenTicker.java deleted file mode 100644 index 655554d7203..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmTokenTicker.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.asfoundation.wallet.repository.entity; - -import io.realm.RealmObject; -import io.realm.annotations.PrimaryKey; - -public class RealmTokenTicker extends RealmObject { - @PrimaryKey private String contract; - private String price; - private String percentChange24h; - private long createdTime; - private String id; - private String image; - private long updatedTime; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getContract() { - return contract; - } - - public void setContract(String contract) { - this.contract = contract; - } - - public String getPrice() { - return price; - } - - public void setPrice(String price) { - this.price = price; - } - - public String getPercentChange24h() { - return percentChange24h; - } - - public void setPercentChange24h(String percentChange24h) { - this.percentChange24h = percentChange24h; - } - - public long getCreatedTime() { - return createdTime; - } - - public void setCreatedTime(long createdTime) { - this.createdTime = createdTime; - } - - public String getImage() { - return image; - } - - public void setImage(String image) { - this.image = image; - } - - public long getUpdatedTime() { - return updatedTime; - } - - public void setUpdatedTime(long updatedTime) { - this.updatedTime = updatedTime; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmTransaction.java b/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmTransaction.java deleted file mode 100644 index 25c68764b18..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmTransaction.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.asfoundation.wallet.repository.entity; - -import io.realm.RealmList; -import io.realm.RealmObject; -import io.realm.annotations.PrimaryKey; - -public class RealmTransaction extends RealmObject { - @PrimaryKey private String hash; - private String blockNumber; - private long timeStamp; - private int nonce; - private String from; - private String to; - private String value; - private String gas; - private String gasPrice; - private String gasUsed; - private String input; - private String error; - private RealmList operations; - - public String getHash() { - return hash; - } - - public void setHash(String hash) { - this.hash = hash; - } - - public String getBlockNumber() { - return blockNumber; - } - - public void setBlockNumber(String blockNumber) { - this.blockNumber = blockNumber; - } - - public long getTimeStamp() { - return timeStamp; - } - - public void setTimeStamp(long timeStamp) { - this.timeStamp = timeStamp; - } - - public int getNonce() { - return nonce; - } - - public void setNonce(int nonce) { - this.nonce = nonce; - } - - public String getFrom() { - return from; - } - - public void setFrom(String from) { - this.from = from; - } - - public String getTo() { - return to; - } - - public void setTo(String to) { - this.to = to; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public String getGas() { - return gas; - } - - public void setGas(String gas) { - this.gas = gas; - } - - public String getGasPrice() { - return gasPrice; - } - - public void setGasPrice(String gasPrice) { - this.gasPrice = gasPrice; - } - - public String getGasUsed() { - return gasUsed; - } - - public void setGasUsed(String gasUsed) { - this.gasUsed = gasUsed; - } - - public String getInput() { - return input; - } - - public void setInput(String input) { - this.input = input; - } - - public String getError() { - return error; - } - - public void setError(String error) { - this.error = error; - } - - public RealmList getOperations() { - return operations; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmTransactionContract.java b/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmTransactionContract.java deleted file mode 100644 index aa9ec996bf4..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmTransactionContract.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.asfoundation.wallet.repository.entity; - -import io.realm.RealmObject; - -public class RealmTransactionContract extends RealmObject { - private String address; - private String name; - private String totalSupply; - private int decimals; - private String symbol; - - public String getAddress() { - return address; - } - - public void setAddress(String address) { - this.address = address; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getTotalSupply() { - return totalSupply; - } - - public void setTotalSupply(String totalSupply) { - this.totalSupply = totalSupply; - } - - public int getDecimals() { - return decimals; - } - - public void setDecimals(int decimals) { - this.decimals = decimals; - } - - public String getSymbol() { - return symbol; - } - - public void setSymbol(String symbol) { - this.symbol = symbol; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmTransactionOperation.java b/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmTransactionOperation.java deleted file mode 100644 index bc174eb67f3..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/repository/entity/RealmTransactionOperation.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.asfoundation.wallet.repository.entity; - -import io.realm.RealmObject; - -public class RealmTransactionOperation extends RealmObject { - private String transactionId; - private String viewType; - private String from; - private String to; - private String value; - private RealmTransactionContract contract; - - public String getTransactionId() { - return transactionId; - } - - public void setTransactionId(String transactionId) { - this.transactionId = transactionId; - } - - public String getViewType() { - return viewType; - } - - public void setViewType(String viewType) { - this.viewType = viewType; - } - - public String getFrom() { - return from; - } - - public void setFrom(String from) { - this.from = from; - } - - public String getTo() { - return to; - } - - public void setTo(String to) { - this.to = to; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public RealmTransactionContract getContract() { - return contract; - } - - public void setContract(RealmTransactionContract contract) { - this.contract = contract; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/entity/TransactionDetailsEntity.kt b/app/src/main/java/com/asfoundation/wallet/repository/entity/TransactionDetailsEntity.kt new file mode 100644 index 00000000000..c8272d94460 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/entity/TransactionDetailsEntity.kt @@ -0,0 +1,19 @@ +package com.asfoundation.wallet.repository.entity + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class TransactionDetailsEntity(@PrimaryKey @Embedded var icon: Icon, + var sourceName: String?, + var description: String?) { + + @Entity + data class Icon(val iconType: Type, @PrimaryKey val uri: String) + + enum class Type { + FILE, URL + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/repository/entity/TransactionEntity.kt b/app/src/main/java/com/asfoundation/wallet/repository/entity/TransactionEntity.kt new file mode 100644 index 00000000000..4599ebc9943 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/repository/entity/TransactionEntity.kt @@ -0,0 +1,41 @@ +package com.asfoundation.wallet.repository.entity + +import androidx.room.Embedded +import androidx.room.Entity + +@Entity(primaryKeys = ["transactionId", "relatedWallet"]) +data class TransactionEntity(val transactionId: String, + val relatedWallet: String, + val approveTransactionId: String?, + val perk: Perk?, + val type: TransactionType, + val subType: SubType?, + val title: String?, + val cardDescription: String?, + val timeStamp: Long, + val processedTime: Long, + val status: TransactionStatus, + val value: String, + val from: String, + val to: String, + @Embedded val details: TransactionDetailsEntity?, + val currency: String?, + val operations: List?) { + + enum class TransactionType { + STANDARD, IAP, ADS, IAP_OFFCHAIN, ADS_OFFCHAIN, BONUS, TOP_UP, TRANSFER_OFF_CHAIN, + ETHER_TRANSFER; + } + + enum class SubType { + PERK_PROMOTION, UNKNOWN + } + + enum class Perk { + GAMIFICATION_LEVEL_UP, PACKAGE_PERK, UNKNOWN + } + + enum class TransactionStatus { + SUCCESS, FAILED, PENDING; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/router/AddTokenRouter.java b/app/src/main/java/com/asfoundation/wallet/router/AddTokenRouter.java deleted file mode 100644 index 88ee714f93a..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/router/AddTokenRouter.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.asfoundation.wallet.router; - -import android.content.Context; -import android.content.Intent; -import com.asfoundation.wallet.ui.AddTokenActivity; - -public class AddTokenRouter { - - public void open(Context context) { - Intent intent = new Intent(context, AddTokenActivity.class); - context.startActivity(intent); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/router/BalanceRouter.kt b/app/src/main/java/com/asfoundation/wallet/router/BalanceRouter.kt new file mode 100644 index 00000000000..c7992d473a4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/router/BalanceRouter.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.router + +import android.content.Context +import android.content.Intent +import com.asfoundation.wallet.ui.balance.BalanceActivity + +class BalanceRouter { + + fun open(context: Context) { + val intent = Intent(context, BalanceActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } + context.startActivity(intent) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/router/ChangeTokenCollectionRouter.java b/app/src/main/java/com/asfoundation/wallet/router/ChangeTokenCollectionRouter.java deleted file mode 100644 index 2e7aa046121..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/router/ChangeTokenCollectionRouter.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.asfoundation.wallet.router; - -import android.content.Context; -import android.content.Intent; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.ui.TokenChangeCollectionActivity; - -import static com.asfoundation.wallet.C.Key.WALLET; - -public class ChangeTokenCollectionRouter { - - public void open(Context context, Wallet wallet) { - Intent intent = new Intent(context, TokenChangeCollectionActivity.class); - intent.putExtra(WALLET, wallet); - context.startActivity(intent); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/router/ConfirmationRouter.java b/app/src/main/java/com/asfoundation/wallet/router/ConfirmationRouter.java index a360d3457b1..7c9ffa55c19 100644 --- a/app/src/main/java/com/asfoundation/wallet/router/ConfirmationRouter.java +++ b/app/src/main/java/com/asfoundation/wallet/router/ConfirmationRouter.java @@ -3,13 +3,15 @@ import android.app.Activity; import android.content.Intent; import com.asfoundation.wallet.entity.TransactionBuilder; +import com.asfoundation.wallet.ui.ActivityResultSharer; import com.asfoundation.wallet.ui.ConfirmationActivity; import io.reactivex.Observable; import io.reactivex.subjects.PublishSubject; +import org.jetbrains.annotations.Nullable; import static com.asfoundation.wallet.C.EXTRA_TRANSACTION_BUILDER; -public class ConfirmationRouter { +public class ConfirmationRouter implements ActivityResultSharer.ActivityResultListener { public static final int TRANSACTION_CONFIRMATION_REQUEST_CODE = 12344; private final PublishSubject result; @@ -24,7 +26,8 @@ public void open(Activity activity, TransactionBuilder transactionBuilder) { activity.startActivityForResult(intent, TRANSACTION_CONFIRMATION_REQUEST_CODE); } - public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + @Override + public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode == TRANSACTION_CONFIRMATION_REQUEST_CODE) { if (resultCode == Activity.RESULT_OK) { result.onNext(new Result(true, requestCode, data)); diff --git a/app/src/main/java/com/asfoundation/wallet/router/ExternalBrowserRouter.java b/app/src/main/java/com/asfoundation/wallet/router/ExternalBrowserRouter.java deleted file mode 100644 index 2741a83cb86..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/router/ExternalBrowserRouter.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.asfoundation.wallet.router; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; - -public class ExternalBrowserRouter { - - public void open(Context context, Uri uri) { - Intent launchBrowser = new Intent(Intent.ACTION_VIEW, uri); - context.startActivity(launchBrowser); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/router/ExternalBrowserRouter.kt b/app/src/main/java/com/asfoundation/wallet/router/ExternalBrowserRouter.kt new file mode 100644 index 00000000000..b0b68ba1877 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/router/ExternalBrowserRouter.kt @@ -0,0 +1,24 @@ +package com.asfoundation.wallet.router + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import com.asf.wallet.R + +class ExternalBrowserRouter { + + fun open(context: Context, uri: Uri) { + try { + val launchBrowser = Intent(Intent.ACTION_VIEW, uri) + launchBrowser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(launchBrowser) + } catch (exception: ActivityNotFoundException) { + exception.printStackTrace() + Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT) + .show() + } + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/router/ImportWalletRouter.java b/app/src/main/java/com/asfoundation/wallet/router/ImportWalletRouter.java deleted file mode 100644 index d718f3ae4a6..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/router/ImportWalletRouter.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.asfoundation.wallet.router; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import com.asfoundation.wallet.ui.ImportWalletActivity; - -public class ImportWalletRouter { - - public void open(Context context) { - Intent intent = new Intent(context, ImportWalletActivity.class); - context.startActivity(intent); - } - - public void openForResult(Activity activity, int requestCode) { - Intent intent = new Intent(activity, ImportWalletActivity.class); - activity.startActivityForResult(intent, requestCode); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/router/ManageWalletsRouter.java b/app/src/main/java/com/asfoundation/wallet/router/ManageWalletsRouter.java deleted file mode 100644 index 445f04e7ad9..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/router/ManageWalletsRouter.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.asfoundation.wallet.router; - -import android.content.Context; -import android.content.Intent; -import com.asfoundation.wallet.ui.WalletsActivity; - -public class ManageWalletsRouter { - - public void open(Context context, boolean isClearStack) { - Intent intent = new Intent(context, WalletsActivity.class); - if (isClearStack) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - } - context.startActivity(intent); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/router/MyAddressRouter.java b/app/src/main/java/com/asfoundation/wallet/router/MyAddressRouter.java index 2c9f206674a..cfe3723d2ca 100644 --- a/app/src/main/java/com/asfoundation/wallet/router/MyAddressRouter.java +++ b/app/src/main/java/com/asfoundation/wallet/router/MyAddressRouter.java @@ -10,8 +10,12 @@ public class MyAddressRouter { public void open(Context context, Wallet wallet) { + if (wallet == null) { + return; + } Intent intent = new Intent(context, MyAddressActivity.class); intent.putExtra(WALLET, wallet); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); context.startActivity(intent); } } diff --git a/app/src/main/java/com/asfoundation/wallet/router/MyTokensRouter.java b/app/src/main/java/com/asfoundation/wallet/router/MyTokensRouter.java deleted file mode 100644 index 246708bdc8a..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/router/MyTokensRouter.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.asfoundation.wallet.router; - -import android.content.Context; -import android.content.Intent; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.ui.TokensActivity; - -import static com.asfoundation.wallet.C.Key.WALLET; - -public class MyTokensRouter { - - public void open(Context context, Wallet wallet) { - Intent intent = new Intent(context, TokensActivity.class); - intent.putExtra(WALLET, wallet); - intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - context.startActivity(intent); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/router/OnboardingRouter.kt b/app/src/main/java/com/asfoundation/wallet/router/OnboardingRouter.kt new file mode 100644 index 00000000000..b79379198f4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/router/OnboardingRouter.kt @@ -0,0 +1,17 @@ +package com.asfoundation.wallet.router + +import android.content.Context +import android.content.Intent +import com.asfoundation.wallet.ui.onboarding.OnboardingActivity + +class OnboardingRouter { + + fun open(context: Context, isClearStack: Boolean) { + val intent = Intent(context, OnboardingActivity::class.java) + if (isClearStack) { + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + context.startActivity(intent) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/router/SendRouter.java b/app/src/main/java/com/asfoundation/wallet/router/SendRouter.java index 77725f40038..bc533f967b8 100644 --- a/app/src/main/java/com/asfoundation/wallet/router/SendRouter.java +++ b/app/src/main/java/com/asfoundation/wallet/router/SendRouter.java @@ -2,21 +2,13 @@ import android.content.Context; import android.content.Intent; -import com.asfoundation.wallet.entity.TokenInfo; -import com.asfoundation.wallet.entity.TransactionBuilder; -import com.asfoundation.wallet.ui.SendActivity; - -import static com.asfoundation.wallet.C.EXTRA_TRANSACTION_BUILDER; +import com.asfoundation.wallet.ui.transact.TransferActivity; public class SendRouter { - public void open(Context context, TokenInfo tokenInfo) { - open(context, new TransactionBuilder(tokenInfo)); - } - - public void open(Context context, TransactionBuilder transactionBuilder) { - Intent intent = new Intent(context, SendActivity.class); - intent.putExtra(EXTRA_TRANSACTION_BUILDER, transactionBuilder); + public void open(Context context) { + Intent intent = TransferActivity.newIntent(context); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); context.startActivity(intent); } } diff --git a/app/src/main/java/com/asfoundation/wallet/router/SettingsRouter.java b/app/src/main/java/com/asfoundation/wallet/router/SettingsRouter.java index 9d6e1b3dbfb..ba4c1b0ea4a 100644 --- a/app/src/main/java/com/asfoundation/wallet/router/SettingsRouter.java +++ b/app/src/main/java/com/asfoundation/wallet/router/SettingsRouter.java @@ -5,9 +5,9 @@ import com.asfoundation.wallet.ui.SettingsActivity; public class SettingsRouter { - public void open(Context context) { Intent intent = new Intent(context, SettingsActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); context.startActivity(intent); } } diff --git a/app/src/main/java/com/asfoundation/wallet/router/TopUpRouter.kt b/app/src/main/java/com/asfoundation/wallet/router/TopUpRouter.kt new file mode 100644 index 00000000000..0ec3ccac85d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/router/TopUpRouter.kt @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.router + +import android.content.Context +import android.content.Intent +import com.asfoundation.wallet.topup.TopUpActivity + +class TopUpRouter { + + fun open(context: Context) { + val intent = TopUpActivity.newIntent(context) + .apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP } + context.startActivity(intent) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/router/TransactionDetailRouter.java b/app/src/main/java/com/asfoundation/wallet/router/TransactionDetailRouter.java index cbe14a65e4e..cce3819be96 100644 --- a/app/src/main/java/com/asfoundation/wallet/router/TransactionDetailRouter.java +++ b/app/src/main/java/com/asfoundation/wallet/router/TransactionDetailRouter.java @@ -3,7 +3,7 @@ import android.content.Context; import android.content.Intent; import com.asfoundation.wallet.transactions.Transaction; -import com.asfoundation.wallet.ui.TransactionDetailActivity; +import com.asfoundation.wallet.ui.balance.TransactionDetailActivity; import static com.asfoundation.wallet.C.Key.TRANSACTION; @@ -12,6 +12,7 @@ public class TransactionDetailRouter { public void open(Context context, Transaction transaction) { Intent intent = new Intent(context, TransactionDetailActivity.class); intent.putExtra(TRANSACTION, transaction); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); context.startActivity(intent); } } diff --git a/app/src/main/java/com/asfoundation/wallet/router/TransactionsRouter.java b/app/src/main/java/com/asfoundation/wallet/router/TransactionsRouter.java index 8a22e812011..fa1adbc4119 100644 --- a/app/src/main/java/com/asfoundation/wallet/router/TransactionsRouter.java +++ b/app/src/main/java/com/asfoundation/wallet/router/TransactionsRouter.java @@ -8,7 +8,7 @@ public class TransactionsRouter { public void open(Context context, boolean isClearStack) { Intent intent = new Intent(context, TransactionsActivity.class); if (isClearStack) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); } context.startActivity(intent); } diff --git a/app/src/main/java/com/asfoundation/wallet/service/AccountKeystoreService.java b/app/src/main/java/com/asfoundation/wallet/service/AccountKeystoreService.java index 40f0cad3a31..cb321fd4b76 100644 --- a/app/src/main/java/com/asfoundation/wallet/service/AccountKeystoreService.java +++ b/app/src/main/java/com/asfoundation/wallet/service/AccountKeystoreService.java @@ -23,20 +23,20 @@ public interface AccountKeystoreService { * * @return included {@link Wallet} if success */ - Single importKeystore(String store, String password, String newPassword); + Single restoreKeystore(String store, String password, String newPassword); - Single importPrivateKey(String privateKey, String newPassword); + Single restorePrivateKey(String privateKey, String newPassword); /** * Export wallet to keystore * - * @param wallet wallet to export + * @param address wallet address to export * @param password password from wallet * @param newPassword new password to store * * @return store data */ - Single exportAccount(Wallet wallet, String password, String newPassword); + Single exportAccount(String address, String password, String newPassword); /** * Delete account from keystore diff --git a/app/src/main/java/com/asfoundation/wallet/service/AccountWalletService.kt b/app/src/main/java/com/asfoundation/wallet/service/AccountWalletService.kt new file mode 100644 index 00000000000..69f943fc1a3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/AccountWalletService.kt @@ -0,0 +1,119 @@ +package com.asfoundation.wallet.service + +import android.util.Pair +import com.appcoins.wallet.bdsbilling.WalletAddressModel +import com.appcoins.wallet.bdsbilling.WalletService +import com.asfoundation.wallet.entity.Wallet +import com.asfoundation.wallet.interact.WalletCreatorInteract +import com.asfoundation.wallet.repository.PasswordStore +import com.asfoundation.wallet.repository.WalletRepositoryType +import com.asfoundation.wallet.util.WalletUtils +import ethereumj.crypto.ECKey +import ethereumj.crypto.HashUtil.sha3 +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.internal.schedulers.ExecutorScheduler +import org.web3j.crypto.Keys.toChecksumAddress + + +class AccountWalletService(private val accountKeyService: AccountKeystoreService, + private val passwordStore: PasswordStore, + private val walletCreatorInteract: WalletCreatorInteract, + private val normalizer: ContentNormalizer, + private val walletRepository: WalletRepositoryType, + private val syncScheduler: ExecutorScheduler) : WalletService { + + private var stringECKeyPair: Pair? = null + + override fun getWalletAddress(): Single { + return find() + .map { wallet -> toChecksumAddress(wallet.address) } + } + + override fun getWalletOrCreate(): Single { + return find() + .subscribeOn(syncScheduler) + .onErrorResumeNext { + walletCreatorInteract.create() + } + .map { wallet -> toChecksumAddress(wallet.address) } + } + + override fun findWalletOrCreate(): Observable { + return find() + .toObservable() + .subscribeOn(syncScheduler) + .map { wallet -> wallet.address } + .onErrorResumeNext { _: Throwable -> + Observable.just(WalletGetterStatus.CREATING.toString()) + .mergeWith( + walletCreatorInteract.create() + .toObservable() + .map { wallet -> wallet.address }) + } + } + + override fun signContent(content: String): Single { + return find() + .flatMap { wallet -> + getPrivateKey(wallet).map { ecKey -> + sign(normalizer.normalize(content), ecKey) + } + } + } + + override fun getAndSignCurrentWalletAddress(): Single { + return find() + .flatMap { wallet -> + getPrivateKey(wallet).map { ecKey -> + sign(normalizer.normalize(toChecksumAddress(wallet.address)), ecKey) + } + .map { WalletAddressModel(wallet.address, it) } + } + } + + @Throws(Exception::class) + fun sign(plainText: String, ecKey: ECKey): String { + val signature = ecKey.sign(sha3(plainText.toByteArray())) + return signature.toHex() + } + + fun find(): Single { + return walletRepository.defaultWallet + .onErrorResumeNext { + walletRepository.fetchWallets() + .filter { wallets: Array -> wallets.isNotEmpty() } + .map { wallets: Array -> + wallets[0] + } + .flatMapCompletable { wallet: Wallet -> + walletRepository.setDefaultWallet(wallet.address) + } + .andThen(walletRepository.defaultWallet) + } + } + + fun create(): Single = walletCreatorInteract.create() + + private fun getPrivateKey(wallet: Wallet): Single { + if (stringECKeyPair != null && stringECKeyPair!!.first.equals(wallet.address, true)) { + return Single.just(stringECKeyPair!!.second) + } + return passwordStore.getPassword(wallet.address) + .flatMap { password -> + accountKeyService.exportAccount(wallet.address, password, password) + .map { json -> + ECKey.fromPrivate(WalletUtils.loadCredentials(password, json) + .ecKeyPair + .privateKey) + } + } + .doOnSuccess { ecKey -> stringECKeyPair = Pair(wallet.address, ecKey) } + } + + interface ContentNormalizer { + fun normalize(content: String): String + } +} + +enum class WalletGetterStatus { CREATING } diff --git a/app/src/main/java/com/asfoundation/wallet/service/AppsApi.kt b/app/src/main/java/com/asfoundation/wallet/service/AppsApi.kt new file mode 100644 index 00000000000..a0cc5cd7e3d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/AppsApi.kt @@ -0,0 +1,16 @@ +package com.asfoundation.wallet.service + +import com.asfoundation.wallet.apps.repository.webservice.data.Application +import io.reactivex.Single +import retrofit2.http.GET + + +interface AppsApi { + companion object { + const val API_BASE_URL = "https://ws75.aptoide.com/api/7/" + } + + @GET( + "listApps/store_name=catappult/group_id=10358961/limit=10/order=rand/sort=sort:appcoins-top-gross-on-top") + fun getApplications(): Single? +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/service/AutoUpdateService.kt b/app/src/main/java/com/asfoundation/wallet/service/AutoUpdateService.kt new file mode 100644 index 00000000000..f56b5907a6f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/AutoUpdateService.kt @@ -0,0 +1,25 @@ +package com.asfoundation.wallet.service + +import com.asfoundation.wallet.entity.AutoUpdateResponse +import com.asfoundation.wallet.viewmodel.AutoUpdateModel +import io.reactivex.Single +import retrofit2.http.GET + +class AutoUpdateService(private val api: AutoUpdateApi) { + + fun loadAutoUpdateModel(): Single { + return api.getAutoUpdateInfo() + .map { + AutoUpdateModel(it.latestVersion.versionCode, it.latestVersion.minSdk, it.blackList) + } + .onErrorReturn { AutoUpdateModel() } + } + + interface AutoUpdateApi { + + @GET("appc/wallet_version") + fun getAutoUpdateInfo(): Single + + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/service/BDSAppsApi.kt b/app/src/main/java/com/asfoundation/wallet/service/BDSAppsApi.kt new file mode 100644 index 00000000000..499d268da7c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/BDSAppsApi.kt @@ -0,0 +1,11 @@ +package com.asfoundation.wallet.service + +import com.asfoundation.wallet.apps.ApplicationsApi +import com.asfoundation.wallet.apps.repository.webservice.data.Application +import io.reactivex.Single + +class BDSAppsApi(val api: AppsApi) : ApplicationsApi { + override fun getApplications(): Single? { + return api.getApplications() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/service/CampaignService.kt b/app/src/main/java/com/asfoundation/wallet/service/CampaignService.kt new file mode 100644 index 00000000000..beec6ff1bd3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/CampaignService.kt @@ -0,0 +1,96 @@ +package com.asfoundation.wallet.service + +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.entity.SubmitPoAException +import com.asfoundation.wallet.entity.SubmitPoAResponse +import com.asfoundation.wallet.poa.PoaInformationModel +import com.asfoundation.wallet.poa.Proof +import com.asfoundation.wallet.poa.ProofComponent +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import retrofit2.http.* + +class CampaignService( + private val campaignApi: CampaignApi, + private val versionCode: Int, + private val scheduler: Scheduler) { + + companion object { + const val SERVICE_HOST = BuildConfig.BACKEND_HOST + } + + fun submitProof(proof: Proof, wallet: String): Single { + return campaignApi.submitProof( + SerializedProof(proof.campaignId, proof.packageName, wallet, proof.proofComponentList, + proof.storeAddress, proof.oemAddress), versionCode) + .map { response -> handleResponse(response) } + .subscribeOn(scheduler) + .singleOrError() + } + + fun getCampaign(address: String, packageName: String, + packageVersionCode: Int): Single { + return campaignApi.getCampaign(address, packageName, packageVersionCode) + .map { response -> handleResponse(response) } + .subscribeOn(scheduler) + .singleOrError() + } + + fun retrievePoaInformation(address: String): Single { + return campaignApi.getPoaInformation(address) + .map { handleResponse(it) } + .subscribeOn(scheduler) + .singleOrError() + } + + private fun handleResponse(response: PoaInformationResponse): PoaInformationModel { + return PoaInformationModel(response.remainingPoa, response.hoursRemaining, + response.minutesRemaining) + } + + private fun handleResponse(response: SubmitPoAResponse): String { + if (response.isValid) { + return response.transactionId + } else { + throw SubmitPoAException(response.errorCode) + } + } + + private fun handleResponse(response: GetCampaignResponse): Campaign { + return if (response.status != GetCampaignResponse.EligibleResponseStatus.NOT_ELIGIBLE && response.bidId != null) { + Campaign(response.bidId, CampaignStatus.AVAILABLE) + } else { + Campaign("", CampaignStatus.NOT_ELIGIBLE, response.hours, response.minutes) + } + } + + interface CampaignApi { + @Headers("Content-Type: application/json") + @POST("/campaign/submitpoa") + fun submitProof(@Body body: SerializedProof?, @Query("version_code") + versionCode: Int): Observable + + @GET("/campaign/remaining_poa") + fun getPoaInformation(@Query("address") address: String): Observable + + @GET("/campaign/eligible") + fun getCampaign(@Query("address") address: String, + @Query("package_name") packageName: String, + @Query("vercode") versionCode: Int): Observable + } +} + +class SerializedProof(val bid_id: String?, + val package_name: String, + val address: String, + val nonces: List, + val store: String, + val oem: String) + +enum class CampaignStatus { + AVAILABLE, NOT_ELIGIBLE +} + +data class Campaign(val campaignId: String, val campaignStatus: CampaignStatus, + val hoursRemaining: Int = 0, val minutesRemaining: Int = 0) diff --git a/app/src/main/java/com/asfoundation/wallet/service/CoinmarketcapTickerService.java b/app/src/main/java/com/asfoundation/wallet/service/CoinmarketcapTickerService.java deleted file mode 100644 index 9b21c2a2a8f..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/service/CoinmarketcapTickerService.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.asfoundation.wallet.service; - -import com.asfoundation.wallet.entity.Ticker; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.TokenTicker; -import com.google.gson.Gson; -import io.reactivex.Observable; -import io.reactivex.ObservableOperator; -import io.reactivex.Observer; -import io.reactivex.Single; -import io.reactivex.annotations.NonNull; -import io.reactivex.observers.DisposableObserver; -import io.reactivex.schedulers.Schedulers; -import okhttp3.OkHttpClient; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.converter.gson.GsonConverterFactory; -import retrofit2.http.GET; -import retrofit2.http.Path; - -public class CoinmarketcapTickerService implements TickerService { - - private static final String COINMARKET_API_URL = "https://api.coinmarketcap.com"; - - private final OkHttpClient httpClient; - private final Gson gson; - private CoinmarketApiClient coinmarketApiClient; - - public CoinmarketcapTickerService(OkHttpClient httpClient, Gson gson) { - this.httpClient = httpClient; - this.gson = gson; - buildApiClient(COINMARKET_API_URL); - } - - private static @NonNull ApiErrorOperator apiError(Gson gson) { - return new ApiErrorOperator<>(); - } - - private void buildApiClient(String baseUrl) { - coinmarketApiClient = new Retrofit.Builder().baseUrl(baseUrl) - .client(httpClient) - .addConverterFactory(GsonConverterFactory.create(gson)) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .build() - .create(CoinmarketApiClient.class); - } - - @Override public Observable fetchTickerPrice(String ticker) { - return coinmarketApiClient.fetchTickerPrice(ticker) - .lift(apiError(gson)) - .map(r -> r[0]) - .subscribeOn(Schedulers.io()); - } - - @Override public Single fetchTockenTickers(Token[] tokens, String currency) { - return Single.just(new TokenTicker[0]); - } - - public interface CoinmarketApiClient { - @GET("/v1/ticker/{ticker}") Observable> fetchTickerPrice( - @Path("ticker") String ticker); - } - - private final static class ApiErrorOperator implements ObservableOperator> { - - @Override public Observer> apply(Observer observer) - throws Exception { - return new DisposableObserver>() { - @Override public void onNext(Response response) { - if (isDisposed()) { - return; - } - observer.onNext(response.body()); - observer.onComplete(); - } - - @Override public void onError(Throwable e) { - if (!isDisposed()) { - observer.onError(e); - } - } - - @Override public void onComplete() { - if (!isDisposed()) { - observer.onComplete(); - } - } - }; - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/service/EthplorerTokenService.java b/app/src/main/java/com/asfoundation/wallet/service/EthplorerTokenService.java deleted file mode 100644 index 3f13d018f60..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/service/EthplorerTokenService.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.asfoundation.wallet.service; - -import com.asfoundation.wallet.entity.TokenInfo; -import com.google.gson.Gson; -import io.reactivex.Observable; -import io.reactivex.schedulers.Schedulers; -import okhttp3.OkHttpClient; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.converter.gson.GsonConverterFactory; -import retrofit2.http.GET; -import retrofit2.http.Path; - -public class EthplorerTokenService implements TokenExplorerClientType { - private static final String ETHPLORER_API_URL = "https://api.ethplorer.io"; - - private EthplorerApiClient ethplorerApiClient; - - public EthplorerTokenService(OkHttpClient httpClient, Gson gson) { - ethplorerApiClient = new Retrofit.Builder().baseUrl(ETHPLORER_API_URL) - .client(httpClient) - .addConverterFactory(GsonConverterFactory.create(gson)) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .build() - .create(EthplorerApiClient.class); - } - - @Override public Observable fetch(String walletAddress) { - return ethplorerApiClient.fetchTokens(walletAddress) - .flatMap(response -> Observable.just(response.body())) - .map(r -> { - if (r.tokens == null) { - return new TokenInfo[0]; - } else { - int len = r.tokens.length; - TokenInfo[] result = new TokenInfo[len]; - for (int i = 0; i < len; i++) { - result[i] = r.tokens[i].tokenInfo; - } - return result; - } - }) - .subscribeOn(Schedulers.io()); - } - - public interface EthplorerApiClient { - @GET("/getAddressInfo/{address}?apiKey=freekey") - Observable> fetchTokens(@Path("address") String address); - } - - private static class Token { - TokenInfo tokenInfo; - } - - private static class EthplorerResponse { - Token[] tokens; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/service/GasService.kt b/app/src/main/java/com/asfoundation/wallet/service/GasService.kt new file mode 100644 index 00000000000..e6c49777aea --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/GasService.kt @@ -0,0 +1,20 @@ +package com.asfoundation.wallet.service + +import com.asf.wallet.BuildConfig +import com.google.gson.annotations.SerializedName +import io.reactivex.Single +import retrofit2.http.GET +import java.math.BigInteger + +interface GasService { + + @GET("transaction/gas_price") + fun getGasPrice(): Single + + companion object { + const val API_BASE_URL = BuildConfig.BACKEND_HOST + } + +} + +data class GasPrice(@SerializedName("gas_price") val price: BigInteger) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/service/GetCampginResponse.kt b/app/src/main/java/com/asfoundation/wallet/service/GetCampginResponse.kt new file mode 100644 index 00000000000..60b363e8cfc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/GetCampginResponse.kt @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.service + +import com.google.gson.annotations.SerializedName + +data class GetCampaignResponse(@SerializedName("status") val status: EligibleResponseStatus, + @SerializedName("bid_id") val bidId: String?, val hours: Int, + val minutes: Int) { + + enum class EligibleResponseStatus { + ELIGIBLE, NOT_ELIGIBLE, REQUIRES_VALIDATION + } +} + + diff --git a/app/src/main/java/com/asfoundation/wallet/service/GethKeystoreAccountService.java b/app/src/main/java/com/asfoundation/wallet/service/GethKeystoreAccountService.java deleted file mode 100644 index 07168d15fa4..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/service/GethKeystoreAccountService.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.asfoundation.wallet.service; - -import com.asfoundation.wallet.C; -import com.asfoundation.wallet.entity.ServiceErrorException; -import com.asfoundation.wallet.entity.ServiceException; -import com.asfoundation.wallet.entity.Wallet; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.reactivex.Completable; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; -import java.io.File; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.nio.charset.Charset; -import org.ethereum.geth.Account; -import org.ethereum.geth.Accounts; -import org.ethereum.geth.Address; -import org.ethereum.geth.BigInt; -import org.ethereum.geth.Geth; -import org.ethereum.geth.KeyStore; -import org.ethereum.geth.Transaction; -import org.json.JSONException; -import org.json.JSONObject; -import org.web3j.crypto.ECKeyPair; -import org.web3j.crypto.WalletFile; - -import static org.web3j.crypto.Wallet.create; - -public class GethKeystoreAccountService implements AccountKeystoreService { - private static final int PRIVATE_KEY_RADIX = 16; - /** - * CPU/Memory cost parameter. Must be larger than 1, a power of 2 and less than 2^(128 * r / 8). - */ - private static final int N = 1 << 9; - /** - * Parallelization parameter. Must be a positive integer less than or equal to Integer.MAX_VALUE / - * (128 * r * 8). - */ - private static final int P = 1; - - private final KeyStore keyStore; - - public GethKeystoreAccountService(File keyStoreFile) { - keyStore = new KeyStore(keyStoreFile.getAbsolutePath(), Geth.LightScryptN, Geth.LightScryptP); - } - - @Override public Single createAccount(String password) { - return Single.fromCallable(() -> new Wallet(keyStore.newAccount(password) - .getAddress() - .getHex() - .toLowerCase())) - .subscribeOn(Schedulers.io()); - } - - @Override - public Single importKeystore(String store, String password, String newPassword) { - return Single.fromCallable(() -> { - String address = extractAddressFromStore(store); - if (hasAccount(address)) { - throw new ServiceErrorException(C.ErrorCode.ALREADY_ADDED, "Already added"); - } - Account account; - try { - account = - keyStore.importKey(store.getBytes(Charset.forName("UTF-8")), password, newPassword); - } catch (Exception ex) { - // We need to make sure that we do not have a broken account - deleteAccount(address, newPassword).subscribe(() -> { - }, t -> { - }); - throw ex; - } - return new Wallet(account.getAddress() - .getHex() - .toLowerCase()); - }) - .subscribeOn(Schedulers.io()); - } - - @Override public Single importPrivateKey(String privateKey, String newPassword) { - return Single.fromCallable(() -> { - BigInteger key = new BigInteger(privateKey, PRIVATE_KEY_RADIX); - ECKeyPair keypair = ECKeyPair.create(key); - WalletFile walletFile = create(newPassword, keypair, N, P); - return new ObjectMapper().writeValueAsString(walletFile); - }) - .compose(upstream -> importKeystore(upstream.blockingGet(), newPassword, newPassword)); - } - - @Override - public Single exportAccount(Wallet wallet, String password, String newPassword) { - return Single.fromCallable(() -> findAccount(wallet.address)) - .flatMap(account1 -> Single.fromCallable( - () -> new String(keyStore.exportKey(account1, password, newPassword)))) - .subscribeOn(Schedulers.io()); - } - - @Override public Completable deleteAccount(String address, String password) { - return Single.fromCallable(() -> findAccount(address)) - .flatMapCompletable( - account -> Completable.fromAction(() -> keyStore.deleteAccount(account, password))) - .subscribeOn(Schedulers.io()); - } - - @Override - public Single signTransaction(String fromAddress, String signerPassword, String toAddress, - BigDecimal amount, BigDecimal gasPrice, BigDecimal gasLimit, long nonce, byte[] data, - long chainId) { - return Single.fromCallable(() -> { - BigInt value = new BigInt(0); - value.setString(amount.toString(), 10); - - BigInt gasPriceBI = new BigInt(0); - gasPriceBI.setString(gasPrice.toString(), 10); - - BigInt gasLimitBI = new BigInt(0); - gasLimitBI.setString(gasLimit.toString(), 10); - - Transaction tx = - new Transaction(nonce, new Address(toAddress), value, gasLimitBI.getInt64(), gasPriceBI, - data); - - BigInt chain = new BigInt(chainId); // Chain identifier of the main net - org.ethereum.geth.Account gethAccount = findAccount(fromAddress); - keyStore.unlock(gethAccount, signerPassword); - Transaction signed = keyStore.signTx(gethAccount, tx, chain); - keyStore.lock(gethAccount.getAddress()); - - return signed.encodeRLP(); - }) - .subscribeOn(Schedulers.io()); - } - - @Override public boolean hasAccount(String address) { - return keyStore.hasAddress(new Address(address)); - } - - @Override public Single fetchAccounts() { - return Single.fromCallable(() -> { - Accounts accounts = keyStore.getAccounts(); - int len = (int) accounts.size(); - Wallet[] result = new Wallet[len]; - - for (int i = 0; i < len; i++) { - org.ethereum.geth.Account gethAccount = accounts.get(i); - result[i] = new Wallet(gethAccount.getAddress() - .getHex() - .toLowerCase()); - } - return result; - }) - .subscribeOn(Schedulers.io()); - } - - private String extractAddressFromStore(String store) throws Exception { - try { - JSONObject jsonObject = new JSONObject(store); - return "0x" + jsonObject.getString("address"); - } catch (JSONException ex) { - throw new Exception("Invalid keystore"); - } - } - - private org.ethereum.geth.Account findAccount(String address) throws ServiceException { - Accounts accounts = keyStore.getAccounts(); - int len = (int) accounts.size(); - for (int i = 0; i < len; i++) { - try { - android.util.Log.d("ACCOUNT_FIND", "Address: " + accounts.get(i) - .getAddress() - .getHex()); - if (accounts.get(i) - .getAddress() - .getHex() - .equalsIgnoreCase(address)) { - return accounts.get(i); - } - } catch (Exception ex) { - /* Quietly: interest only result, maybe next is ok. */ - } - } - throw new ServiceException("Wallet with address: " + address + " not found"); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/service/KeyStoreFileManager.java b/app/src/main/java/com/asfoundation/wallet/service/KeyStoreFileManager.java new file mode 100644 index 00000000000..aea19259d96 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/KeyStoreFileManager.java @@ -0,0 +1,110 @@ +package com.asfoundation.wallet.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import org.web3j.crypto.WalletFile; + +public class KeyStoreFileManager { + private final String keystoreFolderPath; + private final ObjectMapper mapper; + + public KeyStoreFileManager(String keystoreFolderPath, ObjectMapper mapper) { + this.mapper = mapper; + new File(keystoreFolderPath).mkdirs(); + if (keystoreFolderPath.charAt(keystoreFolderPath.length() - 1) == '/') { + this.keystoreFolderPath = keystoreFolderPath; + } else { + this.keystoreFolderPath = keystoreFolderPath + "/"; + } + } + + String getKeystore(String accountAddress) { + return Objects.requireNonNull( + getFilePath(removeHexIndicator(accountAddress), keystoreFolderPath), + "Wallet with address: " + accountAddress + " not found"); + } + + private String removeHexIndicator(String accountAddress) { + return accountAddress.replace("0x", ""); + } + + private String getFilePath(String accountAddress, String path) { + File file = new File(path); + if (file.isDirectory()) { + for (File subFile : file.listFiles()) { + String filePath = getFilePath(accountAddress, subFile.getPath()); + if (filePath != null) { + return filePath; + } + } + } else { + if (file.getName() + .toLowerCase() + .contains(accountAddress.toLowerCase())) { + return file.getAbsolutePath(); + } + } + return null; + } + + String getKeystoreFolderPath() { + return keystoreFolderPath; + } + + /** + * @return keystore file's absolute path + */ + private String saveKeyStoreFile(String keystore, String path) throws IOException { + WalletFile walletFile = mapper.readValue(keystore, WalletFile.class); + File keystoreFile = new File(path.concat(getWalletFileName(walletFile))); + mapper.writeValue(keystoreFile, walletFile); + return keystoreFile.getAbsolutePath(); + } + + /** + * @return keystore file's absolute path + */ + String saveKeyStoreFile(String keystore) throws IOException { + return saveKeyStoreFile(keystore, keystoreFolderPath); + } + + public boolean delete(String keystoreFilePath) { + return new File(keystoreFilePath).delete(); + } + + private String getWalletFileName(WalletFile walletFile) { + SimpleDateFormat dateFormat = new SimpleDateFormat("'UTC--'yyyy-MM-dd'T'HH-mm-ss.SSS'--'"); + return dateFormat.format(new Date()) + walletFile.getAddress() + ".json"; + } + + boolean hasAddress(String address) { + return getFilePath(removeHexIndicator(address), keystoreFolderPath) != null; + } + + List getAccounts() { + List addresses = new ArrayList<>(); + for (File file : new File(keystoreFolderPath).listFiles()) { + String address = getAddressFromFileName(file.getName()); + if (address != null) { + addresses.add(address); + } + } + return addresses; + } + + private String getAddressFromFileName(String fileName) { + try { + String[] split = fileName.split("--"); + return "0x" + split[split.length - 1].split("\\.")[0]; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/service/LocalCurrencyConversionService.java b/app/src/main/java/com/asfoundation/wallet/service/LocalCurrencyConversionService.java new file mode 100644 index 00000000000..342fa29f522 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/LocalCurrencyConversionService.java @@ -0,0 +1,54 @@ +package com.asfoundation.wallet.service; + +import com.asf.wallet.BuildConfig; +import com.asfoundation.wallet.entity.ConversionResponseBody; +import com.asfoundation.wallet.ui.iab.FiatValue; +import io.reactivex.Observable; +import io.reactivex.Single; +import java.math.RoundingMode; +import retrofit2.http.GET; +import retrofit2.http.Path; + +public class LocalCurrencyConversionService { + public static final String CONVERSION_HOST = BuildConfig.BASE_HOST; + + private final LocalCurrencyConversionService.TokenToLocalFiatApi tokenToLocalFiatApi; + + public LocalCurrencyConversionService( + LocalCurrencyConversionService.TokenToLocalFiatApi tokenToLocalFiatApi) { + + this.tokenToLocalFiatApi = tokenToLocalFiatApi; + } + + public Single getLocalCurrency() { + return getAppcToLocalFiat("1.0", 18).firstOrError(); + } + + public Observable getAppcToLocalFiat(String value, int scale) { + return tokenToLocalFiatApi.getValueToLocalFiat(value, "APPC") + .map(response -> new FiatValue(response.getAppcValue() + .setScale(scale, RoundingMode.FLOOR), response.getCurrency(), response.getSymbol())); + } + + public Observable getEtherToLocalFiat(String value, int scale) { + return tokenToLocalFiatApi.getValueToLocalFiat(value, "ETH") + .map(response -> new FiatValue(response.getAppcValue() + .setScale(scale, RoundingMode.FLOOR), response.getCurrency(), response.getSymbol())); + } + + public Observable getLocalToAppc(String currency, String value, int scale) { + return tokenToLocalFiatApi.convertLocalToAppc(currency, value) + .map(response -> new FiatValue(response.getAppcValue() + .setScale(scale, RoundingMode.FLOOR), response.getCurrency(), response.getSymbol())); + } + + public interface TokenToLocalFiatApi { + @GET("broker/8.20180518/exchanges/{valueFrom}/convert/{appcValue}") + Observable getValueToLocalFiat(@Path("appcValue") String appcValue, + @Path("valueFrom") String valueFrom); + + @GET("broker/8.20180518/exchanges/{localCurrency}/convert/{value}?to=APPC") + Observable convertLocalToAppc(@Path("localCurrency") String currency, + @Path("value") String value); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/service/PoaInformationResponse.kt b/app/src/main/java/com/asfoundation/wallet/service/PoaInformationResponse.kt new file mode 100644 index 00000000000..9141f7460ab --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/PoaInformationResponse.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.service + +import com.google.gson.annotations.SerializedName + +data class PoaInformationResponse(@SerializedName("hours") val hoursRemaining: Int, + @SerializedName("remaining_poa") val remainingPoa: Int, + @SerializedName("minutes") val minutesRemaining: Int) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/service/RealmManager.java b/app/src/main/java/com/asfoundation/wallet/service/RealmManager.java deleted file mode 100644 index d3ae1f99ef9..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/service/RealmManager.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.asfoundation.wallet.service; - -import com.asf.wallet.BuildConfig; -import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.Wallet; -import io.realm.Realm; -import io.realm.RealmConfiguration; -import java.util.HashMap; -import java.util.Map; - -public class RealmManager { - - private final Map realmConfigurations = new HashMap<>(); - - public Realm getRealmInstance(NetworkInfo networkInfo, Wallet wallet) { - String name = getName(networkInfo, wallet); - RealmConfiguration config = realmConfigurations.get(name); - if (config == null) { - config = new RealmConfiguration.Builder().name(name) - .schemaVersion(BuildConfig.DB_VERSION) - .deleteRealmIfMigrationNeeded() - .build(); - realmConfigurations.put(name, config); - } - return Realm.getInstance(config); - } - - private String getName(NetworkInfo networkInfo, Wallet wallet) { - return wallet.address + "-" + networkInfo.name + "-db.realm"; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/service/ServicesErrorCodeMapper.kt b/app/src/main/java/com/asfoundation/wallet/service/ServicesErrorCodeMapper.kt new file mode 100644 index 00000000000..0af4969f8b1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/ServicesErrorCodeMapper.kt @@ -0,0 +1,17 @@ +package com.asfoundation.wallet.service + +import com.asf.wallet.R + +class ServicesErrorCodeMapper : ServicesErrorMapper { + + companion object { + const val FORBIDDEN = 403 + } + + override fun mapError(errorCode: Int): Int { + return when (errorCode) { + FORBIDDEN -> R.string.purchase_error_wallet_block_code_403 + else -> R.string.unknown_error + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/service/ServicesErrorMapper.kt b/app/src/main/java/com/asfoundation/wallet/service/ServicesErrorMapper.kt new file mode 100644 index 00000000000..1c4d50793aa --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/ServicesErrorMapper.kt @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.service + +import androidx.annotation.StringRes + +interface ServicesErrorMapper { + @StringRes + fun mapError(errorCode: Int): Int +} diff --git a/app/src/main/java/com/asfoundation/wallet/service/SmsValidationApi.kt b/app/src/main/java/com/asfoundation/wallet/service/SmsValidationApi.kt new file mode 100644 index 00000000000..eb7524adf35 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/SmsValidationApi.kt @@ -0,0 +1,25 @@ +package com.asfoundation.wallet.service + +import com.asfoundation.wallet.entity.WalletRequestCodeResponse +import com.asfoundation.wallet.entity.WalletStatus +import io.reactivex.Single +import retrofit2.http.* + +interface SmsValidationApi { + + @GET("transaction/verified_wallet") + fun isValid(@Query("wallet") wallet: String): Single + + @FormUrlEncoded + @POST("transaction/request_code") + fun requestValidationCode(@Field("phone") phoneNumber: String): Single + + @FormUrlEncoded + @POST("transaction/verify_code") + fun validateCode( + @Field("phone") phoneNumber: String, + @Field("wallet") walletAddress: String, + @Field("code") validationCode: String + ): Single + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/service/TickerService.java b/app/src/main/java/com/asfoundation/wallet/service/TickerService.java deleted file mode 100644 index aca6ad1952c..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/service/TickerService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.asfoundation.wallet.service; - -import com.asfoundation.wallet.entity.Ticker; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.TokenTicker; -import io.reactivex.Observable; -import io.reactivex.Single; - -public interface TickerService { - - Observable fetchTickerPrice(String ticker); - - Single fetchTockenTickers(Token[] tokens, String currency); -} diff --git a/app/src/main/java/com/asfoundation/wallet/service/TokenExplorerClientType.java b/app/src/main/java/com/asfoundation/wallet/service/TokenExplorerClientType.java deleted file mode 100644 index 9f77c4b0db2..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/service/TokenExplorerClientType.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.asfoundation.wallet.service; - -import com.asfoundation.wallet.entity.TokenInfo; -import io.reactivex.Observable; - -public interface TokenExplorerClientType { - Observable fetch(String walletAddress); -} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/service/TokenRateService.java b/app/src/main/java/com/asfoundation/wallet/service/TokenRateService.java new file mode 100644 index 00000000000..8b7e737a663 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/TokenRateService.java @@ -0,0 +1,39 @@ +package com.asfoundation.wallet.service; + +import com.asf.wallet.BuildConfig; +import com.asfoundation.wallet.entity.AppcToFiatResponseBody; +import com.asfoundation.wallet.ui.iab.FiatValue; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; +import retrofit2.http.GET; +import retrofit2.http.Query; + +/** + * Created by franciscocalado on 24/07/2018. + */ + +public class TokenRateService { + public static final String CONVERSION_HOST = BuildConfig.BACKEND_HOST; + + private final TokenToFiatApi tokenToFiatApi; + + public TokenRateService(TokenToFiatApi tokenToFiatApi) { + + this.tokenToFiatApi = tokenToFiatApi; + } + + public Single getAppcRate(String currency) { + return tokenToFiatApi.getAppcToFiatRate(currency) + .map(appcToFiatResponseBody -> appcToFiatResponseBody) + .map(AppcToFiatResponseBody::getFiatValue) + .map(value -> new FiatValue(value, currency, "")) + .subscribeOn(Schedulers.io()) + .singleOrError(); + } + + public interface TokenToFiatApi { + @GET("appc/value") Observable getAppcToFiatRate( + @Query("currency") String currency); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/service/TransactionsNetworkClient.java b/app/src/main/java/com/asfoundation/wallet/service/TransactionsNetworkClient.java deleted file mode 100644 index 92115697a91..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/service/TransactionsNetworkClient.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.asfoundation.wallet.service; - -import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.RawTransaction; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; -import com.google.gson.Gson; -import io.reactivex.Observable; -import io.reactivex.ObservableOperator; -import io.reactivex.Observer; -import io.reactivex.annotations.NonNull; -import io.reactivex.observers.DisposableObserver; -import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.List; -import okhttp3.OkHttpClient; -import retrofit2.Call; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.converter.gson.GsonConverterFactory; -import retrofit2.http.GET; -import retrofit2.http.Query; - -public class TransactionsNetworkClient implements TransactionsNetworkClientType { - - private static final int PAGE_LIMIT = 20; - - private final OkHttpClient httpClient; - private final Gson gson; - - private ApiClient apiClient; - - public TransactionsNetworkClient(OkHttpClient httpClient, Gson gson, - EthereumNetworkRepositoryType networkRepository) { - this.httpClient = httpClient; - this.gson = gson; - - networkRepository.addOnChangeDefaultNetwork(this::onNetworkChanged); - NetworkInfo networkInfo = networkRepository.getDefaultNetwork(); - onNetworkChanged(networkInfo); - } - - private static @NonNull ApiErrorOperator apiError() { - return new ApiErrorOperator<>(); - } - - private void buildApiClient(String baseUrl) { - apiClient = new Retrofit.Builder().baseUrl(baseUrl) - .client(httpClient) - .addConverterFactory(GsonConverterFactory.create(gson)) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .build() - .create(ApiClient.class); - } - - @Override public Observable fetchTransactions(String address) { - return apiClient.fetchTransactions(address) - .lift(apiError()) - .map(r -> r.docs) - .subscribeOn(Schedulers.io()); - } - - @Override public Observable fetchLastTransactions(Wallet wallet, - RawTransaction lastTransaction) { - return Observable.fromCallable(() -> { - @NonNull String lastTransactionHash = lastTransaction == null ? "" : lastTransaction.hash; - List result = new ArrayList<>(); - int pages = 0; - int page = 0; - boolean hasMore = true; - do { - page++; - Call call = - apiClient.fetchTransactions(PAGE_LIMIT, page, wallet.address); - Response response = call.execute(); - if (response.isSuccessful()) { - ApiClientResponse body = response.body(); - if (body != null) { - pages = body.pages; - for (RawTransaction transaction : body.docs) { - if (lastTransactionHash.equals(transaction.hash)) { - hasMore = false; - break; - } - result.add(transaction); - } - } - } - } while (page < pages && hasMore); - return result.toArray(new RawTransaction[result.size()]); - }) - .subscribeOn(Schedulers.io()); - } - - private void onNetworkChanged(NetworkInfo networkInfo) { - buildApiClient(networkInfo.backendUrl); - } - - private interface ApiClient { - @GET("/transactions?limit=50") Observable> fetchTransactions( - @Query("address") String address); - - @GET("/transactions") Call fetchTransactions(@Query("limit") int pageLimit, - @Query("page") int page, @Query("address") String address); - } - - private final static class ApiClientResponse { - RawTransaction[] docs; - int pages; - } - - private final static class ApiErrorOperator implements ObservableOperator> { - - @Override public Observer> apply(Observer observer) { - return new DisposableObserver>() { - @Override public void onNext(Response response) { - observer.onNext(response.body()); - observer.onComplete(); - } - - @Override public void onError(Throwable e) { - observer.onError(e); - } - - @Override public void onComplete() { - observer.onComplete(); - } - }; - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/service/TransactionsNetworkClientType.java b/app/src/main/java/com/asfoundation/wallet/service/TransactionsNetworkClientType.java deleted file mode 100644 index ab8474f7968..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/service/TransactionsNetworkClientType.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.asfoundation.wallet.service; - -import com.asfoundation.wallet.entity.RawTransaction; -import com.asfoundation.wallet.entity.Wallet; -import io.reactivex.Observable; - -public interface TransactionsNetworkClientType { - Observable fetchTransactions(String forAddress); - - Observable fetchLastTransactions(Wallet wallet, RawTransaction lastTransaction); -} diff --git a/app/src/main/java/com/asfoundation/wallet/service/TrustWalletTickerService.java b/app/src/main/java/com/asfoundation/wallet/service/TrustWalletTickerService.java deleted file mode 100644 index 6796af1a116..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/service/TrustWalletTickerService.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.asfoundation.wallet.service; - -import com.asfoundation.wallet.entity.Ticker; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.TokenTicker; -import com.google.gson.Gson; -import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; -import okhttp3.OkHttpClient; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.converter.gson.GsonConverterFactory; -import retrofit2.http.Body; -import retrofit2.http.GET; -import retrofit2.http.POST; -import retrofit2.http.Query; - -public class TrustWalletTickerService implements TickerService { - - private static final String TRUST_API_URL = "https://api.trustwalletapp.com"; - - private final OkHttpClient httpClient; - private final Gson gson; - private ApiClient apiClient; - - public TrustWalletTickerService(OkHttpClient httpClient, Gson gson) { - this.httpClient = httpClient; - this.gson = gson; - buildApiClient(TRUST_API_URL); - } - - private void buildApiClient(String baseUrl) { - apiClient = new Retrofit.Builder().baseUrl(baseUrl) - .client(httpClient) - .addConverterFactory(GsonConverterFactory.create(gson)) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .build() - .create(ApiClient.class); - } - - @Override public Observable fetchTickerPrice(String symbols) { - return apiClient.fetchTickerPrice(symbols) - .map(r -> { - if (r.isSuccessful()) { - TrustResponse body = r.body(); - if (body == null || body.response.length < 0) { - throw new Exception("server error"); - } - return body.response[0]; - } else { - throw new Exception("server error"); - } - }) - .subscribeOn(Schedulers.io()); - } - - @Override public Single fetchTockenTickers(Token[] tokens, String currency) { - return Single.fromCallable(() -> { - if (tokens == null || tokens.length == 0) { - return null; - } - int len = tokens.length; - TokenTickerRequestBody requestBody = new TokenTickerRequestBody(); - requestBody.currency = currency; - requestBody.tokens = new TokenDescriptionRequestBody[len]; - for (int i = 0; i < len; i++) { - requestBody.tokens[i] = new TokenDescriptionRequestBody(tokens[i].tokenInfo.address, - tokens[i].tokenInfo.symbol); - } - return requestBody; - }) - .flatMap(body -> apiClient.fetchTokenPrices(body)) - .map(r -> { - TrustResponse body = r.body(); - return body == null ? null : body.response; - }); - } - - public interface ApiClient { - @GET("prices?currency=USD&") Observable>> fetchTickerPrice( - @Query("symbols") String symbols); - - @POST("tokenPrices") Single>> fetchTokenPrices( - @Body TokenTickerRequestBody body); - } - - private static class TokenTickerRequestBody { - String currency; - TokenDescriptionRequestBody[] tokens; - } - - private static class TokenDescriptionRequestBody { - String contract; - String symbol; - - TokenDescriptionRequestBody(String contract, String symbol) { - this.contract = contract; - this.symbol = symbol; - } - } - - private static class TrustResponse { - T[] response; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/service/WalletBalanceService.kt b/app/src/main/java/com/asfoundation/wallet/service/WalletBalanceService.kt new file mode 100644 index 00000000000..3f5ef85cee7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/WalletBalanceService.kt @@ -0,0 +1,21 @@ +package com.asfoundation.wallet.service + +import com.asf.wallet.BuildConfig +import io.reactivex.Single +import retrofit2.http.GET +import retrofit2.http.Query +import java.math.BigInteger + +interface WalletBalanceService { + + @GET("transaction/balance") + fun getWalletBalance(@Query("wallet") walletAddress: String): Single + + + companion object { + const val API_BASE_URL = BuildConfig.BACKEND_HOST + } + +} + +data class WalletBalance(val appc: BigInteger, val eth: BigInteger) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/service/Web3jKeystoreAccountService.java b/app/src/main/java/com/asfoundation/wallet/service/Web3jKeystoreAccountService.java new file mode 100644 index 00000000000..55de2781625 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/service/Web3jKeystoreAccountService.java @@ -0,0 +1,179 @@ +package com.asfoundation.wallet.service; + +import com.asfoundation.wallet.C; +import com.asfoundation.wallet.entity.ServiceErrorException; +import com.asfoundation.wallet.entity.Wallet; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.reactivex.Completable; +import io.reactivex.Single; +import java.io.File; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; +import org.spongycastle.util.encoders.Hex; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.crypto.WalletFile; +import org.web3j.crypto.WalletUtils; +import org.web3j.tx.ChainId; + +import static org.web3j.crypto.Wallet.create; + +public class Web3jKeystoreAccountService implements AccountKeystoreService { + private static final int PRIVATE_KEY_RADIX = 16; + /** + * CPU/Memory cost parameter. Must be larger than 1, a power of 2 and less than 2^(128 * r / 8). + */ + private static final int N = 1 << 9; + /** + * Parallelization parameter. Must be a positive integer less than or equal to Integer.MAX_VALUE / + * (128 * r * 8). + */ + private static final int P = 1; + + private final KeyStoreFileManager keyStoreFileManager; + private final ObjectMapper objectMapper; + + public Web3jKeystoreAccountService(KeyStoreFileManager keyStoreFileManager, + ObjectMapper objectMapper) { + this.keyStoreFileManager = keyStoreFileManager; + this.objectMapper = objectMapper; + } + + @Override public Single createAccount(String password) { + return Single.fromCallable(() -> WalletUtils.generateNewWalletFile(password, + new File(keyStoreFileManager.getKeystoreFolderPath()), false)) + .map(fileName -> new Wallet(extractAddressFromFileName(fileName))); + } + + @Override + public Single restoreKeystore(String store, String password, String newPassword) { + return Single.fromCallable(() -> extractAddressFromStore(store)) + .flatMap(address -> { + if (hasAccount(address)) { + return Single.error( + new ServiceErrorException(C.ErrorCode.ALREADY_ADDED, "Already added")); + } else { + return importKeystoreInternal(store, password, newPassword); + } + }) + .doOnError(Throwable::printStackTrace); + } + + @Override public Single restorePrivateKey(String privateKey, String newPassword) { + return Single.fromCallable(() -> { + BigInteger key = new BigInteger(privateKey, PRIVATE_KEY_RADIX); + ECKeyPair keypair = ECKeyPair.create(key); + WalletFile walletFile = create(newPassword, keypair, N, P); + return new ObjectMapper().writeValueAsString(walletFile); + }) + .flatMap(keystore -> restoreKeystore(keystore, newPassword, newPassword)); + } + + @Override + public Single exportAccount(String address, String password, String newPassword) { + return Single.fromCallable(() -> keyStoreFileManager.getKeystore(address)) + .map(keystoreFilePath -> WalletUtils.loadCredentials(password, keystoreFilePath)) + .map(credentials -> objectMapper.writeValueAsString( + create(newPassword, credentials.getEcKeyPair(), N, P))); + } + + @Override public Completable deleteAccount(String address, String password) { + return exportAccount(address, password, password).doOnSuccess( + __ -> keyStoreFileManager.delete(keyStoreFileManager.getKeystore(address))) + .ignoreElement(); + } + + @Override + public Single signTransaction(String fromAddress, String signerPassword, String toAddress, + BigDecimal amount, BigDecimal gasPrice, BigDecimal gasLimit, long nonce, byte[] data, + long chainId) { + return Single.fromCallable(() -> { + RawTransaction transaction; + if (data == null) { + transaction = RawTransaction.createEtherTransaction(BigInteger.valueOf(nonce), + gasPrice.toBigInteger(), gasLimit.toBigInteger(), toAddress, amount.toBigInteger()); + } else { + transaction = + RawTransaction.createTransaction(BigInteger.valueOf(nonce), gasPrice.toBigInteger(), + gasLimit.toBigInteger(), toAddress, amount.toBigInteger(), Hex.toHexString(data)); + } + + Credentials credentials = + WalletUtils.loadCredentials(signerPassword, keyStoreFileManager.getKeystore(fromAddress)); + + byte convertedChainId = getChainId(chainId); + return convertedChainId == ChainId.NONE ? TransactionEncoder.signMessage(transaction, + credentials) : TransactionEncoder.signMessage(transaction, convertedChainId, credentials); + }); + } + + @Override public boolean hasAccount(String address) { + return keyStoreFileManager.hasAddress(address); + } + + @Override public Single fetchAccounts() { + return Single.fromCallable(() -> { + List accounts = keyStoreFileManager.getAccounts(); + int len = accounts.size(); + Wallet[] result = new Wallet[len]; + + for (int i = 0; i < len; i++) { + String account = accounts.get(i); + result[i] = new Wallet(account.toLowerCase()); + } + return result; + }); + } + + private Single loadCredentialsFromKeystore(String keystore, String password) { + return Single.fromCallable(() -> { + WalletFile walletFile = objectMapper.readValue(keystore, WalletFile.class); + return Credentials.create(org.web3j.crypto.Wallet.decrypt(password, walletFile)); + }); + } + + private Single importKeystoreInternal(String store, String password, String newPassword) { + return loadCredentialsFromKeystore(store, password).map(credentials -> { + WalletFile walletFile = create(newPassword, credentials.getEcKeyPair(), N, P); + return objectMapper.writeValueAsString(walletFile); + }) + .doOnSuccess(keyStoreFileManager::saveKeyStoreFile) + .map(keystore -> new Wallet(extractAddressFromStore(keystore))) + .doOnError(throwable -> keyStoreFileManager.delete(extractAddressFromStore(store))); + } + + private String extractAddressFromFileName(String fileName) { + String[] split = fileName.split("--"); + return "0x".concat(split[split.length - 1].split("\\.")[0]); + } + + private byte getChainId(long chainId) { + if (chainId == 1) { + return ChainId.MAINNET; + } else if (chainId == 61) { + return ChainId.ETHEREUM_CLASSIC_MAINNET; + } else if (chainId == 42) { + return ChainId.KOVAN; + } else if (chainId == 3) { + return ChainId.ROPSTEN; + } else { + return ChainId.NONE; + } + } + + private String extractAddressFromStore(String keystore) throws Exception { + try { + JsonObject keyStore = JsonParser.parseString(keystore) + .getAsJsonObject(); + return "0x" + keyStore.get("address") + .getAsString(); + } catch (Exception ex) { + throw new Exception("Invalid keystore: " + keystore); + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/support/AlarmManagerBroadcastReceiver.kt b/app/src/main/java/com/asfoundation/wallet/support/AlarmManagerBroadcastReceiver.kt new file mode 100644 index 00000000000..89756935aee --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/support/AlarmManagerBroadcastReceiver.kt @@ -0,0 +1,101 @@ +package com.asfoundation.wallet.support + +import android.app.AlarmManager +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.SystemClock +import androidx.core.app.NotificationCompat +import com.asf.wallet.R +import com.asfoundation.wallet.support.SupportNotificationProperties.ACTION_CHECK_MESSAGES +import com.asfoundation.wallet.support.SupportNotificationProperties.ACTION_DISMISS +import com.asfoundation.wallet.support.SupportNotificationProperties.ACTION_KEY +import com.asfoundation.wallet.support.SupportNotificationProperties.CHANNEL_ID +import com.asfoundation.wallet.support.SupportNotificationProperties.CHANNEL_NAME +import com.asfoundation.wallet.support.SupportNotificationProperties.NOTIFICATION_SERVICE_ID +import dagger.android.AndroidInjection +import dagger.android.DaggerBroadcastReceiver +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class AlarmManagerBroadcastReceiver : DaggerBroadcastReceiver(), HasAndroidInjector { + + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + + @Inject + lateinit var supportInteractor: SupportInteractor + + lateinit var notificationManager: NotificationManager + + companion object { + + @JvmStatic + fun scheduleAlarm(context: Context) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + + val intent = Intent(context, AlarmManagerBroadcastReceiver::class.java) + + val pendingIntent = + PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) + + val repeatInterval = TimeUnit.MINUTES.toMillis(15) + val triggerTime: Long = SystemClock.elapsedRealtime() + repeatInterval + alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime, + repeatInterval, pendingIntent) + } + + } + + override fun androidInjector() = androidInjector + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + AndroidInjection.inject(this, context) + + notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (supportInteractor.shouldShowNotification()) { + supportInteractor.updateUnreadConversations() + notificationManager.notify(NOTIFICATION_SERVICE_ID, createNotification(context).build()) + } + } + + private fun createNotification(context: Context): NotificationCompat.Builder { + val okPendingIntent = createNotificationClickIntent(context) + val dismissPendingIntent = createNotificationDismissIntent(context) + val builder: NotificationCompat.Builder + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val importance = NotificationManager.IMPORTANCE_HIGH + val notificationChannel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance) + builder = NotificationCompat.Builder(context, CHANNEL_ID) + notificationManager.createNotificationChannel(notificationChannel) + } else { + builder = NotificationCompat.Builder(context, CHANNEL_ID) + } + return builder.setContentTitle(context.getString(R.string.support_new_message_title)) + .setAutoCancel(true) + .setContentIntent(okPendingIntent) + .addAction(0, context.getString(R.string.dismiss_button), dismissPendingIntent) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentText(context.getString(R.string.support_new_message_button)) + } + + private fun createNotificationClickIntent(context: Context): PendingIntent { + val intent = SupportNotificationBroadcastReceiver.newIntent(context) + intent.putExtra(ACTION_KEY, ACTION_CHECK_MESSAGES) + return PendingIntent.getBroadcast(context, 0, intent, 0) + } + + private fun createNotificationDismissIntent(context: Context): PendingIntent { + val intent = SupportNotificationBroadcastReceiver.newIntent(context) + intent.putExtra(ACTION_KEY, ACTION_DISMISS) + return PendingIntent.getBroadcast(context, 1, intent, 0) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/support/BootCompletedBroadcastReceiver.kt b/app/src/main/java/com/asfoundation/wallet/support/BootCompletedBroadcastReceiver.kt new file mode 100644 index 00000000000..7d3ec3d9ce6 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/support/BootCompletedBroadcastReceiver.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.support + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class BootCompletedBroadcastReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_BOOT_COMPLETED) { + AlarmManagerBroadcastReceiver.scheduleAlarm(context) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/support/SupportInteractor.kt b/app/src/main/java/com/asfoundation/wallet/support/SupportInteractor.kt new file mode 100644 index 00000000000..2d543553adb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/support/SupportInteractor.kt @@ -0,0 +1,105 @@ +package com.asfoundation.wallet.support + +import com.asfoundation.wallet.App +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.android.gms.tasks.OnCompleteListener +import com.google.android.gms.tasks.Task +import com.google.firebase.iid.FirebaseInstanceId +import com.google.firebase.iid.InstanceIdResult +import io.intercom.android.sdk.Intercom +import io.intercom.android.sdk.UserAttributes +import io.intercom.android.sdk.identity.Registration +import io.intercom.android.sdk.push.IntercomPushClient +import io.reactivex.Observable + + +class SupportInteractor(private val preferences: SupportSharedPreferences, val app: App) { + + companion object { + private const val USER_LEVEL_ATTRIBUTE = "user_level" + } + + private var currentUser = "" + private var currentGamificationLevel = -1 + + fun displayChatScreen() { + resetUnreadConversations() + Intercom.client() + .displayMessenger() + } + + @Suppress("DEPRECATION") + fun displayConversationListOrChat() { + //this method was introduced because if the app is closed intercom returns 0 unread conversations + //even if there are more + resetUnreadConversations() + val handledByIntercom = getUnreadConversations() > 0 + if (handledByIntercom) { + Intercom.client() + .displayMessenger() + } else { + Intercom.client() + .displayConversationsList() + } + } + + fun registerUser(level: Int, walletAddress: String) { + if (currentUser != walletAddress || currentGamificationLevel != level) { + if (currentUser != walletAddress) { + Intercom.client() + .logout() + } + + val userAttributes = UserAttributes.Builder() + .withName(walletAddress) + .withCustomAttribute(USER_LEVEL_ATTRIBUTE, + level + 1)//we set level + 1 to help with readability for the support team + .build() + val registration: Registration = Registration.create() + .withUserId(walletAddress) + .withUserAttributes(userAttributes) + + val gpsAvailable = checkGooglePlayServices() + if (gpsAvailable) handleFirebaseToken() + + Intercom.client() + .registerIdentifiedUser(registration) + currentUser = walletAddress + currentGamificationLevel = level + } + } + + fun getUnreadConversationCountListener() = Observable.create { + Intercom.client() + .addUnreadConversationCountListener { unreadCount -> it.onNext(unreadCount) } + } + + fun getUnreadConversationCount() = Observable.just(Intercom.client().unreadConversationCount) + + fun shouldShowNotification() = + getUnreadConversations() > preferences.checkSavedUnreadConversations() + + fun updateUnreadConversations() = preferences.updateUnreadConversations(getUnreadConversations()) + + private fun resetUnreadConversations() = preferences.resetUnreadConversations() + + private fun getUnreadConversations() = Intercom.client().unreadConversationCount + + private fun checkGooglePlayServices(): Boolean { + val availability = GoogleApiAvailability.getInstance() + return availability.isGooglePlayServicesAvailable(app) == ConnectionResult.SUCCESS + } + + private fun handleFirebaseToken() { + FirebaseInstanceId.getInstance() + .instanceId + .addOnCompleteListener(object : OnCompleteListener { + override fun onComplete(task: Task) { + if (!task.isSuccessful) return + IntercomPushClient().sendTokenToIntercom(app, task.result?.token!!) + } + }) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/support/SupportMessagingService.kt b/app/src/main/java/com/asfoundation/wallet/support/SupportMessagingService.kt new file mode 100644 index 00000000000..e3679995431 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/support/SupportMessagingService.kt @@ -0,0 +1,81 @@ +package com.asfoundation.wallet.support + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.os.Build +import androidx.core.app.NotificationCompat +import com.asf.wallet.R +import com.asfoundation.wallet.support.SupportNotificationProperties.ACTION_CHECK_MESSAGES +import com.asfoundation.wallet.support.SupportNotificationProperties.ACTION_DISMISS +import com.asfoundation.wallet.support.SupportNotificationProperties.ACTION_KEY +import com.asfoundation.wallet.support.SupportNotificationProperties.CHANNEL_ID +import com.asfoundation.wallet.support.SupportNotificationProperties.CHANNEL_NAME +import com.asfoundation.wallet.support.SupportNotificationProperties.NOTIFICATION_SERVICE_ID +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import io.intercom.android.sdk.push.IntercomPushClient + + +class SupportMessagingService : FirebaseMessagingService() { + + private lateinit var notificationManager: NotificationManager + private lateinit var intercomPushClient: IntercomPushClient + + override fun onCreate() { + super.onCreate() + intercomPushClient = IntercomPushClient() + } + + override fun onNewToken(token: String) = + intercomPushClient.sendTokenToIntercom(application, token) + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (intercomPushClient.isIntercomPush(remoteMessage.data)) { + if (isSupportMessage(remoteMessage.data)) { + notificationManager.notify(NOTIFICATION_SERVICE_ID, createNotification(this).build()) + } + } + } + + private fun createNotification(context: Context): NotificationCompat.Builder { + val okPendingIntent = createNotificationClickIntent(context) + val dismissPendingIntent = createNotificationDismissIntent(context) + val builder: NotificationCompat.Builder + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val importance = NotificationManager.IMPORTANCE_HIGH + val notificationChannel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance) + builder = NotificationCompat.Builder(context, CHANNEL_ID) + notificationManager.createNotificationChannel(notificationChannel) + } else { + builder = NotificationCompat.Builder(context, CHANNEL_ID) + } + return builder.setContentTitle(context.getString(R.string.support_new_message_title)) + .setAutoCancel(true) + .setContentIntent(okPendingIntent) + .addAction(0, context.getString(R.string.dismiss_button), dismissPendingIntent) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentText(context.getString(R.string.support_new_message_button)) + } + + private fun createNotificationClickIntent(context: Context): PendingIntent { + val intent = SupportNotificationBroadcastReceiver.newIntent(context) + intent.putExtra(ACTION_KEY, ACTION_CHECK_MESSAGES) + return PendingIntent.getBroadcast(context, 0, intent, 0) + } + + private fun createNotificationDismissIntent(context: Context): PendingIntent { + val intent = SupportNotificationBroadcastReceiver.newIntent(context) + intent.putExtra(ACTION_KEY, ACTION_DISMISS) + return PendingIntent.getBroadcast(context, 1, intent, 0) + } + + private fun isSupportMessage(data: MutableMap): Boolean { + val type = data["conversation_part_type"] + return type != null && (type == "message" || type == "comment") + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/support/SupportNotificationBroadcastReceiver.kt b/app/src/main/java/com/asfoundation/wallet/support/SupportNotificationBroadcastReceiver.kt new file mode 100644 index 00000000000..399cdb2cf4c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/support/SupportNotificationBroadcastReceiver.kt @@ -0,0 +1,50 @@ +package com.asfoundation.wallet.support + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.asfoundation.wallet.support.SupportNotificationProperties.ACTION_CHECK_MESSAGES +import com.asfoundation.wallet.support.SupportNotificationProperties.ACTION_DISMISS +import com.asfoundation.wallet.support.SupportNotificationProperties.ACTION_KEY +import com.asfoundation.wallet.support.SupportNotificationProperties.NOTIFICATION_SERVICE_ID +import com.asfoundation.wallet.ui.TransactionsActivity + +class SupportNotificationBroadcastReceiver : BroadcastReceiver() { + + private lateinit var notificationManager: NotificationManager + + companion object { + + @JvmStatic + fun newIntent(context: Context) = + Intent(context, SupportNotificationBroadcastReceiver::class.java) + } + + override fun onReceive(context: Context, intent: Intent) { + notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + notificationManager.cancel(NOTIFICATION_SERVICE_ID) + context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) + + when (intent.getStringExtra(ACTION_KEY)) { + ACTION_CHECK_MESSAGES -> onNotificationClicked(context) + ACTION_DISMISS -> return + } + } + + private fun onNotificationClicked(context: Context) { + navigateToIntercomScreen(context) + } + + private fun navigateToIntercomScreen(context: Context) { + val transactionsIntent = TransactionsActivity.newIntent(context, true) + .apply { + addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(transactionsIntent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/support/SupportNotificationProperties.kt b/app/src/main/java/com/asfoundation/wallet/support/SupportNotificationProperties.kt new file mode 100644 index 00000000000..9cf5cacc24f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/support/SupportNotificationProperties.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.support + +object SupportNotificationProperties { + + const val NOTIFICATION_SERVICE_ID = 77794 + const val CHANNEL_ID = "support_notification_channel_id" + const val CHANNEL_NAME = "Support notification channel" + + const val ACTION_KEY = "ACTION_KEY" + const val ACTION_CHECK_MESSAGES = "ACTION_CHECK_MESSAGES" + const val ACTION_DISMISS = "ACTION_DISMISS" + const val SUPPORT_NOTIFICATION_CLICK = "SUPPORT_NOTIFICATION_CLICK" +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/support/SupportSharedPreferences.kt b/app/src/main/java/com/asfoundation/wallet/support/SupportSharedPreferences.kt new file mode 100644 index 00000000000..9a3ec386fba --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/support/SupportSharedPreferences.kt @@ -0,0 +1,29 @@ +package com.asfoundation.wallet.support + +import android.content.SharedPreferences + +class SupportSharedPreferences(private val sharedPreferences: SharedPreferences) { + + companion object { + private const val UNREAD_CONVERSATIONS = "UNREAD_CONVERSATIONS" + } + + fun checkSavedUnreadConversations() = sharedPreferences.getInt(UNREAD_CONVERSATIONS, 0) + + fun updateUnreadConversations(unreadConversations: Int) { + sharedPreferences.edit() + .apply { + putInt(UNREAD_CONVERSATIONS, unreadConversations) + apply() + } + } + + fun resetUnreadConversations() { + sharedPreferences.edit() + .apply { + putInt(UNREAD_CONVERSATIONS, 0) + apply() + } + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/LocalCurrency.kt b/app/src/main/java/com/asfoundation/wallet/topup/LocalCurrency.kt new file mode 100644 index 00000000000..fcad5061afe --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/LocalCurrency.kt @@ -0,0 +1,5 @@ +package com.asfoundation.wallet.topup + +import java.io.Serializable + +data class LocalCurrency(val symbol: String = "", val code: String = "") : Serializable diff --git a/app/src/main/java/com/asfoundation/wallet/topup/LocalTopUpPaymentFragment.kt b/app/src/main/java/com/asfoundation/wallet/topup/LocalTopUpPaymentFragment.kt new file mode 100644 index 00000000000..99c150e8d43 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/LocalTopUpPaymentFragment.kt @@ -0,0 +1,297 @@ +package com.asfoundation.wallet.topup + +import android.animation.Animator +import android.content.Context +import android.graphics.Bitmap +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.navigator.UriNavigator +import com.asfoundation.wallet.topup.payment.PaymentFragmentNavigator +import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor +import com.asfoundation.wallet.ui.iab.LocalPaymentInteractor +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.error_top_up_layout.* +import kotlinx.android.synthetic.main.fragment_adyen_top_up.converted_value +import kotlinx.android.synthetic.main.fragment_adyen_top_up.loading +import kotlinx.android.synthetic.main.fragment_adyen_top_up.main_currency_code +import kotlinx.android.synthetic.main.fragment_adyen_top_up.main_value +import kotlinx.android.synthetic.main.local_topup_payment_layout.* +import kotlinx.android.synthetic.main.no_network_retry_only_layout.* +import kotlinx.android.synthetic.main.pending_user_payment_view.* +import kotlinx.android.synthetic.main.support_error_layout.view.* +import kotlinx.android.synthetic.main.topup_pending_user_payment_view.view.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class LocalTopUpPaymentFragment : DaggerFragment(), LocalTopUpPaymentView { + + @Inject + lateinit var localPaymentInteractor: LocalPaymentInteractor + + @Inject + lateinit var formatter: CurrencyFormatUtils + + @Inject + lateinit var topUpAnalytics: TopUpAnalytics + + @Inject + lateinit var inAppPurchaseInteractor: InAppPurchaseInteractor + + @Inject + lateinit var logger: Logger + + private lateinit var activityView: TopUpActivityView + private lateinit var presenter: LocalTopUpPaymentPresenter + private lateinit var navigator: PaymentFragmentNavigator + private var minFrame = 0 + private var maxFrame = 40 + + companion object { + + private const val PAYMENT_ID = "payment_id" + private const val PAYMENT_ICON = "payment_icon" + private const val PAYMENT_LABEL = "payment_label" + private const val PAYMENT_DATA = "data" + private const val ANIMATION_STEP_ONE_START_FRAME = 0 + private const val ANIMATION_STEP_TWO_START_FRAME = 80 + private const val ANIMATION_FRAME_INCREMENT = 40 + private const val BUTTON_ANIMATION_START_FRAME = 120 + + fun newInstance(paymentId: String, icon: String, label: String, + data: TopUpPaymentData): LocalTopUpPaymentFragment { + val fragment = LocalTopUpPaymentFragment() + Bundle().apply { + putString(PAYMENT_ID, paymentId) + putString(PAYMENT_ICON, icon) + putString(PAYMENT_LABEL, label) + putSerializable(PAYMENT_DATA, data) + fragment.arguments = this + } + return fragment + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check(context is TopUpActivityView) { + throw IllegalStateException("Local topup payment fragment must be attached to Topup activity") + } + activityView = context + navigator = PaymentFragmentNavigator((activity as UriNavigator?)!!, activityView) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = LocalTopUpPaymentPresenter(this, activityView, context, localPaymentInteractor, + topUpAnalytics, navigator, formatter, inAppPurchaseInteractor.billingMessagesMapper, + AndroidSchedulers.mainThread(), Schedulers.io(), CompositeDisposable(), data, paymentId, + paymentIcon, activity!!.packageName, logger) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.local_topup_payment_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present(savedInstanceState) + } + + override fun showValues(value: String, currency: String, appcValue: String) { + main_value.visibility = View.VISIBLE + if (data.selectedCurrencyType == TopUpData.FIAT_CURRENCY) { + main_value.setText(value) + main_currency_code.text = currency + converted_value.text = "$appcValue ${WalletCurrency.CREDITS.symbol}" + } else { + main_value.setText(appcValue) + main_currency_code.text = WalletCurrency.CREDITS.symbol + converted_value.text = "$value $currency" + } + } + + override fun showError() { + activityView.unlockRotation() + loading.visibility = View.GONE + main_content.visibility = View.GONE + no_network.visibility = View.GONE + topup_pending_user_payment_view.visibility = View.GONE + error_view?.error_message?.text = getString(R.string.unknown_error) + error_view?.visibility = View.VISIBLE + } + + override fun showNetworkError() { + activityView.unlockRotation() + loading.visibility = View.GONE + main_content.visibility = View.GONE + topup_pending_user_payment_view.visibility = View.GONE + error_view?.visibility = View.GONE + no_network.visibility = View.VISIBLE + } + + override fun getSupportIconClicks() = RxView.clicks(layout_support_icn) + + override fun getSupportLogoClicks() = RxView.clicks(layout_support_logo) + + override fun getGotItClick() = RxView.clicks(got_it_button) + + override fun getTryAgainClick() = RxView.clicks(try_again) + + override fun retryClick() = RxView.clicks(retry_button) + + override fun showRetryAnimation() { + retry_button.visibility = View.INVISIBLE + retry_animation.visibility = View.VISIBLE + } + + override fun showProcessingLoading() { + loading.visibility = View.VISIBLE + topup_pending_user_payment_view.visibility = View.GONE + main_content.visibility = View.GONE + error_view?.visibility = View.GONE + no_network.visibility = View.GONE + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + presenter.onSaveInstanceState(outState) + } + + override fun showPendingUserPayment(paymentMethodIcon: Bitmap) { + activityView.unlockRotation() + loading.visibility = View.GONE + error_view?.visibility = View.GONE + no_network.visibility = View.GONE + main_content.visibility = View.GONE + topup_pending_user_payment_view.visibility = View.VISIBLE + val placeholder = getString(R.string.async_steps_topup_2) + val stepOneText = String.format(placeholder, paymentLabel) + + step_one_desc.text = stepOneText + + topup_pending_user_payment_view?.top_up_in_progress_animation?.updateBitmap("image_0", + paymentMethodIcon) + + playAnimation() + } + + private fun playAnimation() { + topup_pending_user_payment_view?.top_up_in_progress_animation?.setMinAndMaxFrame(minFrame, + maxFrame) + topup_pending_user_payment_view?.top_up_in_progress_animation?.addAnimatorListener(object : + Animator.AnimatorListener { + override fun onAnimationRepeat(animation: Animator?) = Unit + + override fun onAnimationEnd(animation: Animator?) { + if (minFrame == BUTTON_ANIMATION_START_FRAME) { + topup_pending_user_payment_view?.top_up_in_progress_animation?.cancelAnimation() + } else { + minFrame += ANIMATION_FRAME_INCREMENT + maxFrame += ANIMATION_FRAME_INCREMENT + topup_pending_user_payment_view?.top_up_in_progress_animation?.setMinAndMaxFrame(minFrame, + maxFrame) + topup_pending_user_payment_view?.top_up_in_progress_animation?.playAnimation() + } + } + + override fun onAnimationCancel(animation: Animator?) = Unit + + override fun onAnimationStart(animation: Animator?) { + when (minFrame) { + ANIMATION_STEP_ONE_START_FRAME -> { + animateShow(step_one) + animateShow(step_one_desc) + } + ANIMATION_STEP_TWO_START_FRAME -> { + animateShow(step_two) + animateShow(step_two_desc, got_it_button) + } + else -> return + } + } + }) + topup_pending_user_payment_view?.top_up_in_progress_animation?.playAnimation() + } + + private fun animateShow(view: View, viewToAnimateInTheEnd: View? = null) { + view.apply { + alpha = 0.0f + visibility = View.VISIBLE + + animate() + .alpha(1f) + .withEndAction { + this.visibility = View.VISIBLE + viewToAnimateInTheEnd?.let { animateButton(it) } + } + .setDuration(TimeUnit.SECONDS.toMillis(1)) + .setListener(null) + } + } + + private fun animateButton(view: View) { + view.apply { + alpha = 0.2f + animate() + .alpha(1f) + .withEndAction { this.isClickable = true } + .setDuration(TimeUnit.SECONDS.toMillis(1)) + .setListener(null) + } + } + + override fun navigateToPaymentSelection() = activityView.navigateBack() + + override fun onDestroyView() { + super.onDestroyView() + presenter.stop() + } + + override fun onDestroy() { + activityView.unlockRotation() + super.onDestroy() + } + + private val paymentId: String by lazy { + if (arguments!!.containsKey(PAYMENT_ID)) { + arguments!!.getString(PAYMENT_ID)!! + } else { + throw IllegalArgumentException("payment id data not found") + } + } + + private val paymentIcon: String by lazy { + if (arguments!!.containsKey(PAYMENT_ICON)) { + arguments!!.getString(PAYMENT_ICON)!! + } else { + throw IllegalArgumentException("payment icon data not found") + } + } + + private val paymentLabel: String by lazy { + if (arguments!!.containsKey(PAYMENT_LABEL)) { + arguments!!.getString(PAYMENT_LABEL)!! + } else { + throw IllegalArgumentException("payment label data not found") + } + } + + private val data: TopUpPaymentData by lazy { + if (arguments!!.containsKey(PAYMENT_DATA)) { + arguments!!.getSerializable(PAYMENT_DATA)!! as TopUpPaymentData + } else { + throw IllegalArgumentException("topup payment data not found") + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/LocalTopUpPaymentPresenter.kt b/app/src/main/java/com/asfoundation/wallet/topup/LocalTopUpPaymentPresenter.kt new file mode 100644 index 00000000000..a5e4fd6f125 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/LocalTopUpPaymentPresenter.kt @@ -0,0 +1,236 @@ +package com.asfoundation.wallet.topup + +import android.content.Context +import android.graphics.Bitmap +import android.os.Bundle +import android.util.TypedValue +import com.appcoins.wallet.bdsbilling.repository.entity.Transaction +import com.appcoins.wallet.billing.BillingMessagesMapper +import com.asf.wallet.R +import com.asfoundation.wallet.GlideApp +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.ui.iab.LocalPaymentInteractor +import com.asfoundation.wallet.ui.iab.Navigator +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.asfoundation.wallet.util.isNoNetworkException +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.TimeUnit + +class LocalTopUpPaymentPresenter( + private val view: LocalTopUpPaymentView, + private val activityView: TopUpActivityView, + private val context: Context?, + private val localPaymentInteractor: LocalPaymentInteractor, + private val analytics: TopUpAnalytics, + private val navigator: Navigator, + private val formatter: CurrencyFormatUtils, + private val billingMessagesMapper: BillingMessagesMapper, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val data: TopUpPaymentData, + private val paymentId: String, + private val paymentIcon: String, + private val packageName: String, + private val logger: Logger) { + + private var waitingResult: Boolean = false + private var status: ViewState = ViewState.NONE + + fun present(savedInstance: Bundle?) { + setupUi() + if (savedInstance != null) { + waitingResult = savedInstance.getBoolean(WAITING_RESULT) + status = savedInstance.get(STATUS_KEY) as ViewState + } + handleViewInitialization(status) + handlePaymentRedirect() + handleTryAgainClick() + handleGotItClick() + handleSupportClicks() + handleRetryClick() + } + + private fun handleViewInitialization(status: ViewState) { + when (status) { + ViewState.PENDING_USER_PAYMENT -> preparePendingUserPayment() + ViewState.GENERIC_ERROR -> view.showError() + ViewState.NO_NETWORK -> view.showNetworkError() + ViewState.LOADING -> view.showProcessingLoading() + ViewState.NONE -> onViewCreatedRequestLink() + } + } + + private fun setupUi() { + val fiatAmount = formatter.formatCurrency(data.fiatValue, WalletCurrency.FIAT) + val appcAmount = formatter.formatCurrency(data.appcValue, WalletCurrency.CREDITS) + view.showValues(fiatAmount, data.fiatCurrencyCode, appcAmount) + } + + private fun preparePendingUserPayment() { + disposables.add(getPaymentMethodIcon() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { + status = ViewState.PENDING_USER_PAYMENT + view.showPendingUserPayment(it) + } + .subscribe({}, { showError(it) })) + } + + private fun getPaymentMethodIcon(): Single { + return Single.fromCallable { + GlideApp.with(context!!) + .asBitmap() + .load(paymentIcon) + .override(getWidth(), getHeight()) + .centerCrop() + .submit() + .get() + } + } + + private fun onViewCreatedRequestLink() { + disposables.add(localPaymentInteractor.getTopUpPaymentLink(packageName, data.fiatValue, + data.fiatCurrencyCode, paymentId, context?.getString(R.string.topup_title) ?: "Top up") + .filter { !waitingResult && it.isNotEmpty() } + .observeOn(viewScheduler) + .doOnSuccess { + analytics.sendConfirmationEvent(data.appcValue.toDouble(), "top_up", paymentId) + navigator.navigateToUriForResult(it) + waitingResult = true + } + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .subscribe({ }, { showError(it) })) + } + + private fun handlePaymentRedirect() { + disposables.add(navigator.uriResults() + .doOnNext { + activityView.lockOrientation() + status = ViewState.LOADING + view.showProcessingLoading() + } + .flatMap { + localPaymentInteractor.getTransaction(it) + .subscribeOn(networkScheduler) + } + .observeOn(viewScheduler) + .flatMapCompletable { handleTransactionStatus(it) } + .subscribe({}, { showError(it) })) + } + + private fun handleTryAgainClick() { + disposables.add(view.getTryAgainClick() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .doOnNext { view.navigateToPaymentSelection() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleGotItClick() { + disposables.add(view.getGotItClick() + .doOnNext { activityView.close() } + .subscribe({}, { activityView.close() }) + ) + } + + private fun handleTransactionStatus(transaction: Transaction): Completable { + return when { + isErrorStatus(transaction) -> Completable.fromAction { + logger.log(TAG, "Transaction came with error status: ${transaction.status}") + showGenericError() + } + transaction.status == Transaction.Status.COMPLETED -> handleSyncCompletedStatus() + localPaymentInteractor.isAsync(transaction.type) -> + Completable.fromAction { + analytics.sendSuccessEvent(data.appcValue.toDouble(), paymentId, "pending") + } + .andThen(Completable.fromAction { preparePendingUserPayment() }) + else -> Completable.complete() + } + } + + private fun isErrorStatus(transaction: Transaction) = + transaction.status == Transaction.Status.FAILED || + transaction.status == Transaction.Status.CANCELED || + transaction.status == Transaction.Status.INVALID_TRANSACTION + + private fun handleSyncCompletedStatus(): Completable { + return Completable.fromAction { + analytics.sendSuccessEvent(data.appcValue.toDouble(), paymentId, "success") + val bundle = createBundle(data.fiatValue, data.fiatCurrencyCode, data.fiatCurrencySymbol) + waitingResult = false + navigator.popView(bundle) + } + } + + private fun handleSupportClicks() { + disposables.add(Observable.merge(view.getSupportIconClicks(), view.getSupportLogoClicks()) + .throttleFirst(50, TimeUnit.MILLISECONDS) + .flatMapCompletable { localPaymentInteractor.showSupport(data.gamificationLevel) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun showError(throwable: Throwable) { + logger.log(TAG, throwable) + if (throwable.isNoNetworkException()) { + status = ViewState.NO_NETWORK + view.showNetworkError() + } else showGenericError() + } + + private fun showGenericError() { + status = ViewState.GENERIC_ERROR + view.showError() + } + + private fun createBundle(priceAmount: String, priceCurrency: String, + fiatCurrencySymbol: String): Bundle { + return billingMessagesMapper.topUpBundle(priceAmount, priceCurrency, + data.bonusValue.toPlainString(), fiatCurrencySymbol) + } + + fun onSaveInstanceState(outState: Bundle) { + outState.putSerializable(STATUS_KEY, status) + outState.putBoolean(WAITING_RESULT, waitingResult) + } + + private fun handleRetryClick() { + disposables.add(view.retryClick() + .observeOn(viewScheduler) + .doOnNext { view.showRetryAnimation() } + .delay(1, TimeUnit.SECONDS) + .doOnNext { view.navigateToPaymentSelection() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun getWidth(): Int { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 184f, + context?.resources?.displayMetrics) + .toInt() + } + + private fun getHeight(): Int { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 80f, + context?.resources?.displayMetrics) + .toInt() + } + + fun stop() { + waitingResult = false + disposables.clear() + } + + companion object { + private val TAG = LocalTopUpPaymentPresenter::class.java.simpleName + private const val WAITING_RESULT = "WAITING_RESULT" + private const val STATUS_KEY = "status" + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/LocalTopUpPaymentView.kt b/app/src/main/java/com/asfoundation/wallet/topup/LocalTopUpPaymentView.kt new file mode 100644 index 00000000000..e4362aa0883 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/LocalTopUpPaymentView.kt @@ -0,0 +1,34 @@ +package com.asfoundation.wallet.topup + +import android.graphics.Bitmap +import io.reactivex.Observable + +interface LocalTopUpPaymentView { + fun showValues(value: String, currency: String, appcValue: String) + + fun showError() + + fun getSupportIconClicks(): Observable + + fun getSupportLogoClicks(): Observable + + fun getGotItClick(): Observable + + fun getTryAgainClick(): Observable + + fun retryClick(): Observable + + fun showProcessingLoading() + + fun showPendingUserPayment(paymentMethodIcon: Bitmap) + + fun navigateToPaymentSelection() + + fun showNetworkError() + + fun showRetryAnimation() +} + +enum class ViewState { + NONE, PENDING_USER_PAYMENT, GENERIC_ERROR, NO_NETWORK, LOADING +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/PaymentTypeInfo.kt b/app/src/main/java/com/asfoundation/wallet/topup/PaymentTypeInfo.kt new file mode 100644 index 00000000000..bd04b9c84f1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/PaymentTypeInfo.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.topup + +import com.asfoundation.wallet.billing.adyen.PaymentType +import java.io.Serializable + +data class PaymentTypeInfo(val paymentType: PaymentType, val paymentId: String, val label: String, + val icon: String) : Serializable diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpActivity.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpActivity.kt new file mode 100644 index 00000000000..1303d6acce5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpActivity.kt @@ -0,0 +1,270 @@ +package com.asfoundation.wallet.topup + +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.net.Uri +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import com.appcoins.wallet.billing.AppcoinsBillingBinder +import com.asf.wallet.R +import com.asfoundation.wallet.backup.BackupNotificationUtils +import com.asfoundation.wallet.billing.adyen.PaymentType +import com.asfoundation.wallet.navigator.UriNavigator +import com.asfoundation.wallet.permissions.manage.view.ToolbarManager +import com.asfoundation.wallet.router.TransactionsRouter +import com.asfoundation.wallet.topup.address.BillingAddressTopUpFragment +import com.asfoundation.wallet.topup.payment.AdyenTopUpFragment +import com.asfoundation.wallet.transactions.PerkBonusService +import com.asfoundation.wallet.ui.BaseActivity +import com.asfoundation.wallet.ui.iab.WebViewActivity +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import com.asfoundation.wallet.wallet_validation.generic.WalletValidationActivity +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxrelay2.PublishRelay +import dagger.android.AndroidInjection +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.error_top_up_layout.* +import kotlinx.android.synthetic.main.support_error_layout.error_message +import kotlinx.android.synthetic.main.support_error_layout.layout_support_icn +import kotlinx.android.synthetic.main.support_error_layout.layout_support_logo +import kotlinx.android.synthetic.main.top_up_activity_layout.* +import java.util.* +import javax.inject.Inject + +class TopUpActivity : BaseActivity(), TopUpActivityView, ToolbarManager, UriNavigator { + + @Inject + lateinit var topUpInteractor: TopUpInteractor + + @Inject + lateinit var topUpAnalytics: TopUpAnalytics + + @Inject + lateinit var walletBlockedInteract: WalletBlockedInteract + + private lateinit var results: PublishRelay + private lateinit var presenter: TopUpActivityPresenter + private var isFinishingPurchase = false + private var firstImpression = true + + companion object { + @JvmStatic + fun newIntent(context: Context) = Intent(context, TopUpActivity::class.java) + + const val WEB_VIEW_REQUEST_CODE = 1234 + const val WALLET_VALIDATION_REQUEST_CODE = 1235 + const val BILLING_ADDRESS_REQUEST_CODE = 1236 + const val BILLING_ADDRESS_SUCCESS_CODE = 1000 + const val BILLING_ADDRESS_CANCEL_CODE = 1001 + const val ERROR_MESSAGE = "error_message" + private const val TOP_UP_AMOUNT = "top_up_amount" + private const val TOP_UP_CURRENCY = "currency" + private const val TOP_UP_CURRENCY_SYMBOL = "currency_symbol" + private const val BONUS = "bonus" + private const val FIRST_IMPRESSION = "first_impression" + } + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.top_up_activity_layout) + presenter = TopUpActivityPresenter(this, topUpInteractor, AndroidSchedulers.mainThread(), + Schedulers.io(), CompositeDisposable()) + results = PublishRelay.create() + presenter.present(savedInstanceState == null) + if (savedInstanceState != null && savedInstanceState.containsKey(FIRST_IMPRESSION)) { + firstImpression = savedInstanceState.getBoolean(FIRST_IMPRESSION) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + presenter.processActivityResult(requestCode, resultCode, data) + } + + override fun showTopUpScreen() { + toolbar() + handleTopUpStartAnalytics() + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, TopUpFragment.newInstance(packageName)) + .commit() + layout_error.visibility = View.GONE + fragment_container.visibility = View.VISIBLE + } + + override fun showWalletValidation(@StringRes error: Int) { + fragment_container.visibility = View.GONE + val intent = WalletValidationActivity.newIntent(this, error) + .apply { + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + startActivityForResult(intent, WALLET_VALIDATION_REQUEST_CODE) + } + + override fun showError(@StringRes error: Int) { + layout_error.visibility = View.VISIBLE + error_message.text = getText(error) + } + + override fun getSupportClicks(): Observable { + return Observable.merge(RxView.clicks(layout_support_logo), RxView.clicks(layout_support_icn)) + } + + override fun navigateToAdyenPayment(paymentType: PaymentType, data: TopUpPaymentData) { + supportFragmentManager.beginTransaction() + .add(R.id.fragment_container, + AdyenTopUpFragment.newInstance(paymentType, data)) + .addToBackStack(AdyenTopUpFragment::class.java.simpleName) + .commit() + } + + override fun navigateToLocalPayment(paymentId: String, icon: String, label: String, + topUpData: TopUpPaymentData) { + supportFragmentManager.beginTransaction() + .add(R.id.fragment_container, + LocalTopUpPaymentFragment.newInstance(paymentId, icon, label, topUpData)) + .addToBackStack(LocalTopUpPaymentFragment::class.java.simpleName) + .commit() + } + + override fun navigateToBillingAddress(topUpData: TopUpPaymentData, fiatAmount: String, + fiatCurrency: String, targetFragment: Fragment, + shouldStoreCard: Boolean, preSelected: Boolean) { + val fragment = BillingAddressTopUpFragment.newInstance(topUpData, fiatAmount, fiatCurrency, + shouldStoreCard, preSelected) + .apply { + setTargetFragment(targetFragment, BILLING_ADDRESS_REQUEST_CODE) + } + supportFragmentManager.beginTransaction() + .add(R.id.fragment_container, fragment) + .addToBackStack(BillingAddressTopUpFragment::class.java.simpleName) + .commit() + } + + override fun onBackPressed() { + when { + isFinishingPurchase -> close() + supportFragmentManager.backStackEntryCount != 0 -> supportFragmentManager.popBackStack() + else -> super.onBackPressed() + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + when { + isFinishingPurchase -> close() + supportFragmentManager.backStackEntryCount != 0 -> supportFragmentManager.popBackStack() + else -> super.onBackPressed() + } + return true + } + return super.onOptionsItemSelected(item) + } + + override fun navigateBack() { + if (supportFragmentManager.backStackEntryCount != 0) { + supportFragmentManager.popBackStack() + } else { + close() + } + } + + override fun setupToolbar() { + toolbar() + } + + override fun popBackStack() { + if (supportFragmentManager.backStackEntryCount != 0) { + supportFragmentManager.popBackStack() + } + } + + override fun launchPerkBonusService(address: String) { + PerkBonusService.buildService(this, address) + } + + override fun finishActivity(data: Bundle) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + TopUpSuccessFragment.newInstance(data.getString(TOP_UP_AMOUNT, ""), + data.getString(TOP_UP_CURRENCY, ""), data.getString(BONUS, ""), + data.getString(TOP_UP_CURRENCY_SYMBOL, "")), + TopUpSuccessFragment::class.java.simpleName) + .commit() + unlockRotation() + } + + override fun showBackupNotification(walletAddress: String) { + BackupNotificationUtils.showBackupNotification(this, walletAddress) + } + + override fun finish(data: Bundle) { + if (data.getInt(AppcoinsBillingBinder.RESPONSE_CODE) == AppcoinsBillingBinder.RESULT_OK) { + presenter.handleBackupNotifications(data) + presenter.handlePerkNotifications(data) + } else { + finishActivity(data) + } + } + + override fun close(navigateToTransactions: Boolean) { + if (supportFragmentManager.findFragmentByTag( + TopUpSuccessFragment::class.java.simpleName) != null && navigateToTransactions) { + TransactionsRouter().open(this, true) + } + finish() + } + + override fun acceptResult(uri: Uri) { + results.accept(Objects.requireNonNull(uri, "Intent data cannot be null!")) + } + + override fun navigateToUri(url: String) { + startActivityForResult(WebViewActivity.newIntent(this, url), WEB_VIEW_REQUEST_CODE) + } + + override fun showToolbar() = setupToolbar() + + override fun uriResults() = results + + override fun unlockRotation() { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } + + override fun lockOrientation() { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED + } + + override fun getTryAgainClicks() = RxView.clicks(try_again) + + override fun setFinishingPurchase() { + isFinishingPurchase = true + } + + override fun cancelPayment() { + if (supportFragmentManager.backStackEntryCount != 0) { + supportFragmentManager.popBackStackImmediate() + } else { + super.onBackPressed() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(FIRST_IMPRESSION, firstImpression) + } + + private fun handleTopUpStartAnalytics() { + if (firstImpression) { + topUpAnalytics.sendStartEvent() + firstImpression = false + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpActivityPresenter.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpActivityPresenter.kt new file mode 100644 index 00000000000..9ac42a2fb13 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpActivityPresenter.kt @@ -0,0 +1,107 @@ +package com.asfoundation.wallet.topup + +import android.content.Intent +import android.os.Bundle +import androidx.annotation.StringRes +import com.asf.wallet.R +import com.asfoundation.wallet.topup.TopUpActivity.Companion.WALLET_VALIDATION_REQUEST_CODE +import com.asfoundation.wallet.ui.iab.IabActivity +import com.asfoundation.wallet.ui.iab.WebViewActivity +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.TimeUnit + +class TopUpActivityPresenter(private val view: TopUpActivityView, + private val topUpInteractor: TopUpInteractor, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val disposables: CompositeDisposable) { + fun present(isCreating: Boolean) { + if (isCreating) { + view.showTopUpScreen() + } + handleSupportClicks() + handleTryAgainClicks() + } + + private fun handleSupportClicks() { + disposables.add(view.getSupportClicks() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .flatMapCompletable { topUpInteractor.showSupport() } + .subscribe({}, { handleError(it) }) + ) + } + + private fun handleTryAgainClicks() { + disposables.add(view.getTryAgainClicks() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .doOnNext { view.showTopUpScreen() } + .subscribe({}, { handleError(it) }) + ) + } + + fun processActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == TopUpActivity.WEB_VIEW_REQUEST_CODE) { + if (resultCode == WebViewActivity.SUCCESS && data != null) { + data.data?.let { view.acceptResult(it) } ?: view.cancelPayment() + } else if (resultCode == WebViewActivity.FAIL) { + view.cancelPayment() + } + } else if (requestCode == WALLET_VALIDATION_REQUEST_CODE) { + var errorMessage = data?.getIntExtra(IabActivity.ERROR_MESSAGE, 0) + if (errorMessage == null || errorMessage == 0) { + errorMessage = R.string.unknown_error + } + handleWalletBlockedCheck(errorMessage) + } + } + + private fun handleWalletBlockedCheck(@StringRes error: Int) { + disposables.add( + topUpInteractor.isWalletBlocked() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { + view.popBackStack() + if (it) view.showError(error) + else view.showTopUpScreen() + } + .subscribe({}, { handleError(it) }) + ) + } + + private fun handleError(throwable: Throwable) { + throwable.printStackTrace() + view.showError(R.string.unknown_error) + } + + fun handlePerkNotifications(bundle: Bundle) { + disposables.add(topUpInteractor.getWalletAddress() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { + view.launchPerkBonusService(it) + view.finishActivity(bundle) + } + .doOnError { view.finishActivity(bundle) } + .subscribe({}, { it.printStackTrace() })) + } + + + fun handleBackupNotifications(bundle: Bundle) { + disposables.add(topUpInteractor.incrementAndValidateNotificationNeeded() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { notificationNeeded -> + if (notificationNeeded.isNeeded) { + view.showBackupNotification(notificationNeeded.walletAddress) + } + view.finishActivity(bundle) + } + .doOnError { view.finish(bundle) } + .subscribe({ }, { it.printStackTrace() }) + ) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpActivityView.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpActivityView.kt new file mode 100644 index 00000000000..abe799587fb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpActivityView.kt @@ -0,0 +1,55 @@ +package com.asfoundation.wallet.topup + +import android.net.Uri +import android.os.Bundle +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import com.asfoundation.wallet.billing.adyen.PaymentType +import io.reactivex.Observable + +interface TopUpActivityView { + fun showTopUpScreen() + + fun navigateToAdyenPayment(paymentType: PaymentType, data: TopUpPaymentData) + + fun navigateToLocalPayment(paymentId: String, icon: String, label: String, + topUpData: TopUpPaymentData) + + fun navigateToBillingAddress(topUpData: TopUpPaymentData, fiatAmount: String, + fiatCurrency: String, targetFragment: Fragment, + shouldStoreCard: Boolean, preSelected: Boolean) + + fun finish(data: Bundle) + + fun finishActivity(data: Bundle) + + fun showBackupNotification(walletAddress: String) + + fun navigateBack() + + fun close(navigateToTransactions: Boolean = true) + + fun acceptResult(uri: Uri) + + fun showToolbar() + + fun lockOrientation() + + fun unlockRotation() + + fun cancelPayment() + + fun setFinishingPurchase() + + fun showError(@StringRes error: Int) + + fun getSupportClicks(): Observable + + fun showWalletValidation(@StringRes error: Int) + + fun getTryAgainClicks(): Observable + + fun popBackStack() + + fun launchPerkBonusService(address: String) +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpAdapter.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpAdapter.kt new file mode 100644 index 00000000000..1cabe773254 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpAdapter.kt @@ -0,0 +1,50 @@ +package com.asfoundation.wallet.topup + +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.asf.wallet.R +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.util.NumberFormatterUtils +import kotlinx.android.synthetic.main.item_top_value.view.* +import rx.functions.Action1 + + +class TopUpAdapter(private val listener: Action1 +) : ListAdapter(FiatValueCallback()) { + + + class FiatValueCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: FiatValue, newItem: FiatValue): Boolean { + return oldItem.amount == newItem.amount + } + + override fun areContentsTheSame(oldItem: FiatValue, newItem: FiatValue): Boolean { + return oldItem == newItem + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopUpViewHolder { + val inflater = LayoutInflater.from(parent.context) + return TopUpViewHolder(inflater.inflate(R.layout.item_top_value, parent, false)) + } + + override fun onBindViewHolder(holder: TopUpViewHolder, position: Int) { + holder.bind(getItem(position), listener) + } + + class TopUpViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + fun bind(fiatValue: FiatValue, listener: Action1) { + val formatter = NumberFormatterUtils.create() + val text = fiatValue.symbol + formatter.formatNumberWithSuffix(fiatValue.amount.toFloat()) + + itemView.value.text = text + itemView.setOnClickListener { listener.call(fiatValue) } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpAnalytics.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpAnalytics.kt new file mode 100644 index 00000000000..c7241d8f396 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpAnalytics.kt @@ -0,0 +1,92 @@ +package com.asfoundation.wallet.topup + +import cm.aptoide.analytics.AnalyticsManager + +class TopUpAnalytics(private val analyticsManager: AnalyticsManager) { + + fun sendStartEvent() { + analyticsManager.logEvent(HashMap(), WALLET_TOP_UP_START, + AnalyticsManager.Action.CLICK, WALLET) + } + + fun sendSelectionEvent(value: Double, action: String, paymentMethod: String) { + val map = topUpBaseMap(value, paymentMethod) + + map[ACTION] = action + + analyticsManager.logEvent(map, WALLET_TOP_UP_SELECTION, AnalyticsManager.Action.CLICK, WALLET) + } + + fun sendConfirmationEvent(value: Double, action: String, paymentMethod: String) { + val map = topUpBaseMap(value, paymentMethod) + + map[ACTION] = action + + analyticsManager.logEvent(map, WALLET_TOP_UP_CONFIRMATION, AnalyticsManager.Action.CLICK, + WALLET) + } + + fun sendSuccessEvent(value: Double, paymentMethod: String, status: String) { + val map = topUpBaseMap(value, paymentMethod) + + map[STATUS] = status + + analyticsManager.logEvent(map, WALLET_TOP_UP_CONCLUSION, AnalyticsManager.Action.CLICK, + WALLET) + } + + fun sendPaypalUrlEvent(value: Double, paymentMethod: String, type: String?, resultCode: String?, + url: String) { + val map = topUpBaseMap(value, paymentMethod) + + type?.let { map[PAYPAL_TYPE] = it } + resultCode?.let { map[RESULT_CODE] = it } + if (url.length > MAX_CHARACTERS) { + map[URL] = url.takeLast(MAX_CHARACTERS) + } else { + map[URL] = url + } + + analyticsManager.logEvent(map, WALLET_TOP_UP_PAYPAL_URL, AnalyticsManager.Action.CLICK, WALLET) + } + + fun sendErrorEvent(value: Double, paymentMethod: String, status: String, + errorCode: String, errorDetails: String) { + val map = topUpBaseMap(value, paymentMethod) + + map[STATUS] = status + map[ERROR_CODE] = errorCode + map[ERROR_DETAILS] = errorDetails + + analyticsManager.logEvent(map, WALLET_TOP_UP_CONCLUSION, AnalyticsManager.Action.CLICK, + WALLET) + } + + private fun topUpBaseMap(value: Double, paymentMethod: String): HashMap { + val map = HashMap() + + map[VALUE] = value + map[METHOD] = paymentMethod + + return map + } + + companion object { + const val WALLET_TOP_UP_START = "wallet_top_up_start" + const val WALLET_TOP_UP_SELECTION = "wallet_top_up_selection" + const val WALLET_TOP_UP_CONFIRMATION = "wallet_top_up_confirmation" + const val WALLET_TOP_UP_CONCLUSION = "wallet_top_up_conclusion" + const val WALLET_TOP_UP_PAYPAL_URL = "wallet_top_up_conclusion_paypal" + private const val VALUE = "value" + private const val ACTION = "action" + private const val METHOD = "payment_method" + private const val STATUS = "status" + private const val ERROR_CODE = "error_code" + private const val ERROR_DETAILS = "error_details" + private const val PAYPAL_TYPE = "type" + private const val RESULT_CODE = "result_code" + private const val URL = "url" + private const val WALLET = "wallet" + private const val MAX_CHARACTERS = 100 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpData.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpData.kt new file mode 100644 index 00000000000..516c909ffdb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpData.kt @@ -0,0 +1,23 @@ +package com.asfoundation.wallet.topup + +import com.asfoundation.wallet.topup.TopUpData.Companion.DEFAULT_VALUE +import java.io.Serializable +import java.math.BigDecimal + +data class TopUpData(var currency: CurrencyData, + var selectedCurrencyType: String, + var paymentMethod: PaymentTypeInfo? = null, + var bonusValue: BigDecimal = BigDecimal.ZERO) : + Serializable { + companion object { + const val FIAT_CURRENCY = "FIAT_CURRENCY" + const val APPC_C_CURRENCY = "APPC_C_CURRENCY" + const val DEFAULT_VALUE = "--" + } +} + +data class CurrencyData(var fiatCurrencyCode: String = DEFAULT_VALUE, + var fiatCurrencySymbol: String = DEFAULT_VALUE, + var fiatValue: String = DEFAULT_VALUE, var appcCode: String = DEFAULT_VALUE, + var appcSymbol: String = DEFAULT_VALUE, + var appcValue: String = DEFAULT_VALUE) : Serializable diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpDefaultValuesResponse.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpDefaultValuesResponse.kt new file mode 100644 index 00000000000..341fadc0d0b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpDefaultValuesResponse.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.topup + +data class TopUpDefaultValuesResponse(val items: List) { + + data class TopUpDefaultValueBody(val uid: String, val label: String, val description: String, + val price: Price) + + data class Price(val fiat: Fiat) + + data class Fiat(val value: String, val currency: Currency) + + data class Currency(val code: String, val sign: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpFragment.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpFragment.kt new file mode 100644 index 00000000000..e9e1596243b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpFragment.kt @@ -0,0 +1,608 @@ +package com.asfoundation.wallet.topup + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.os.Bundle +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.view.animation.AccelerateDecelerateInterpolator +import android.view.animation.Animation +import android.view.animation.RotateAnimation +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.asf.wallet.R +import com.asfoundation.wallet.billing.adyen.PaymentType +import com.asfoundation.wallet.topup.TopUpData.Companion.APPC_C_CURRENCY +import com.asfoundation.wallet.topup.TopUpData.Companion.DEFAULT_VALUE +import com.asfoundation.wallet.topup.TopUpData.Companion.FIAT_CURRENCY +import com.asfoundation.wallet.topup.paymentMethods.TopUpPaymentMethodsAdapter +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.ui.iab.PaymentMethod +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxbinding2.widget.RxTextView +import com.jakewharton.rxrelay2.PublishRelay +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.fragment_top_up.* +import kotlinx.android.synthetic.main.no_network_retry_only_layout.* +import kotlinx.android.synthetic.main.view_purchase_bonus.view.* +import rx.functions.Action1 +import java.math.BigDecimal +import javax.inject.Inject + +class TopUpFragment : DaggerFragment(), TopUpFragmentView { + + @Inject + lateinit var interactor: TopUpInteractor + + @Inject + lateinit var topUpAnalytics: TopUpAnalytics + + @Inject + lateinit var formatter: CurrencyFormatUtils + + private lateinit var adapter: TopUpPaymentMethodsAdapter + private lateinit var presenter: TopUpFragmentPresenter + private lateinit var paymentMethodClick: PublishRelay + private lateinit var fragmentContainer: ViewGroup + private lateinit var paymentMethods: List + private lateinit var topUpAdapter: TopUpAdapter + private lateinit var keyboardEvents: PublishSubject + private var valueSubject: PublishSubject? = null + private var topUpActivityView: TopUpActivityView? = null + private var selectedCurrency = FIAT_CURRENCY + private var switchingCurrency = false + private var bonusValue = BigDecimal.ZERO + private var localCurrency = LocalCurrency() + private var selectedPaymentMethodId: String? = null + + companion object { + private const val PARAM_APP_PACKAGE = "APP_PACKAGE" + private const val APPC_C_SYMBOL = "APPC-C" + + private const val SELECTED_VALUE_PARAM = "SELECTED_VALUE" + private const val SELECTED_PAYMENT_METHOD_PARAM = "SELECTED_PAYMENT_METHOD" + private const val SELECTED_CURRENCY_PARAM = "SELECTED_CURRENCY" + private const val LOCAL_CURRENCY_PARAM = "LOCAL_CURRENCY" + + + @JvmStatic + fun newInstance(packageName: String): TopUpFragment { + val bundle = Bundle().apply { + putString(PARAM_APP_PACKAGE, packageName) + } + return TopUpFragment().apply { + arguments = bundle + } + } + } + + private val listener = ViewTreeObserver.OnGlobalLayoutListener { + val fragmentView = this.view + val appBarHeight = getAppBarHeight() + fragmentView?.let { + val heightDiff: Int = it.rootView.height - it.height - appBarHeight + + val threshold = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 150f, + requireContext().resources.displayMetrics) + .toInt() + + keyboardEvents.onNext(heightDiff > threshold) + } + } + + private val appPackage: String by lazy { + if (arguments!!.containsKey(PARAM_APP_PACKAGE)) { + arguments!!.getString(PARAM_APP_PACKAGE)!! + } else { + throw IllegalArgumentException("application package name data not found") + } + } + + override fun onDetach() { + super.onDetach() + topUpActivityView = null + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check( + context is TopUpActivityView) { "TopUp fragment must be attached to TopUp activity" } + topUpActivityView = context + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + paymentMethodClick = PublishRelay.create() + valueSubject = PublishSubject.create() + keyboardEvents = PublishSubject.create() + presenter = TopUpFragmentPresenter(this, topUpActivityView, interactor, + AndroidSchedulers.mainThread(), Schedulers.io(), CompositeDisposable(), topUpAnalytics, + formatter, savedInstanceState?.getString(SELECTED_VALUE_PARAM)) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + fragmentContainer = container!! + return inflater.inflate(R.layout.fragment_top_up, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + topUpActivityView?.showToolbar() + savedInstanceState?.let { + if (savedInstanceState.containsKey(SELECTED_CURRENCY_PARAM)) { + selectedCurrency = savedInstanceState.getString(SELECTED_CURRENCY_PARAM) ?: FIAT_CURRENCY + localCurrency = savedInstanceState.getSerializable(LOCAL_CURRENCY_PARAM) as LocalCurrency + } + selectedPaymentMethodId = it.getString(SELECTED_PAYMENT_METHOD_PARAM) + } + presenter.present(appPackage, savedInstanceState) + + topUpAdapter = TopUpAdapter(Action1 { valueSubject?.onNext(it) }) + rv_default_values.apply { + adapter = topUpAdapter + } + view.viewTreeObserver.addOnGlobalLayoutListener(listener) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(SELECTED_VALUE_PARAM, main_value.text.toString()) + if (::adapter.isInitialized) { + outState.putString(SELECTED_PAYMENT_METHOD_PARAM, adapter.getSelectedItemData().id) + } + outState.putString(SELECTED_CURRENCY_PARAM, selectedCurrency) + outState.putSerializable(LOCAL_CURRENCY_PARAM, localCurrency) + presenter.onSavedInstance(outState) + } + + override fun setupPaymentMethods(paymentMethods: List) { + this@TopUpFragment.paymentMethods = paymentMethods + adapter = TopUpPaymentMethodsAdapter(paymentMethods, paymentMethodClick) + selectPaymentMethod(paymentMethods) + + payment_methods.adapter = adapter + + handlePaymentListMaxHeight(paymentMethods.size) + + payments_skeleton.visibility = View.GONE + payment_methods.visibility = View.VISIBLE + } + + private fun handlePaymentListMaxHeight(listSize: Int) { + if (listSize > 2) { + val orientation = resources.configuration.orientation + val params: LayoutParams = payment_methods.layoutParams as LayoutParams + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + params.height = dpToPx(164f) + } + if (orientation == Configuration.ORIENTATION_PORTRAIT && paymentMethods.size > 3) { + params.height = dpToPx(228f) + } + payment_methods.layoutParams = params + } + } + + private fun selectPaymentMethod(paymentMethods: List) { + var selected = false + if (selectedPaymentMethodId != null) { + for (i in paymentMethods.indices) { + if (paymentMethods[i].id == selectedPaymentMethodId && paymentMethods[i].isEnabled) { + selectedPaymentMethodId = paymentMethods[i].id + adapter.setSelectedItem(i) + selected = true + } + } + } + if (!selected) adapter.setSelectedItem(0) + } + + override fun setupCurrency(localCurrency: LocalCurrency) { + hideErrorViews() + if (isLocalCurrencyValid(localCurrency)) { + this@TopUpFragment.localCurrency = localCurrency + setupCurrencyData(selectedCurrency, localCurrency.code, DEFAULT_VALUE, APPC_C_SYMBOL, + DEFAULT_VALUE) + } + main_value.isEnabled = true + main_value.setMinTextSize(resources.getDimensionPixelSize(R.dimen.topup_main_value_min_size) + .toFloat()) + main_value.setOnEditorActionListener { _, actionId, _ -> + if (EditorInfo.IME_ACTION_NEXT == actionId) { + hideKeyboard() + button.performClick() + } + true + } + top_separator_topup.visibility = View.VISIBLE + bot_separator.visibility = View.VISIBLE + swap_value_button.isEnabled = true + swap_value_button.visibility = View.VISIBLE + swap_value_label.visibility = View.VISIBLE + //added since this fragment continues active after navigating to the payment fragment + if (fragmentManager?.backStackEntryCount == 0) focusAndShowKeyboard(main_value) + + } + + private fun focusAndShowKeyboard(view: EditText) { + view.post { + view.requestFocus() + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.showSoftInput(view, InputMethodManager.SHOW_FORCED) + } + } + + override fun setDefaultAmountValue(amount: String) { + setupCurrencyData(selectedCurrency, localCurrency.code, amount, APPC_C_SYMBOL, DEFAULT_VALUE) + } + + override fun setValuesAdapter(values: List) { + val addMargin = values.size <= getTopUpValuesSpanCount() + rv_default_values.addItemDecoration( + DividerItemDecoration(context, LinearLayoutManager.HORIZONTAL)) + rv_default_values.addItemDecoration(TopUpItemDecorator(values.size, addMargin)) + + topUpAdapter.submitList(values) + } + + override fun showValuesAdapter() { + if (rv_default_values.visibility == View.GONE) { + rv_default_values.visibility = View.VISIBLE + bottom_separator.visibility = View.VISIBLE + } + } + + override fun hideValuesAdapter() { + if (rv_default_values.visibility == View.VISIBLE) { + rv_default_values.visibility = View.GONE + bottom_separator.visibility = View.GONE + } + } + + override fun getKeyboardEvents(): Observable { + return keyboardEvents + } + + override fun onPause() { + hideKeyboard() + super.onPause() + } + + override fun onDestroy() { + view?.viewTreeObserver?.removeOnGlobalLayoutListener(listener) + presenter.stop() + super.onDestroy() + } + + override fun getChangeCurrencyClick(): Observable { + return RxView.clicks(swap_value_button) + } + + override fun disableSwapCurrencyButton() { + swap_value_button.isEnabled = false + } + + override fun enableSwapCurrencyButton() { + swap_value_button.isEnabled = true + } + + override fun getValuesClicks() = valueSubject!! + + override fun getEditTextChanges(): Observable { + return RxTextView.afterTextChangeEvents(main_value) + .filter { !switchingCurrency } + .map { + TopUpData(getCurrencyData(), selectedCurrency, getSelectedPaymentMethod()) + } + } + + override fun getPaymentMethodClick(): Observable { + return paymentMethodClick + } + + override fun getNextClick(): Observable { + return RxView.clicks(button) + .map { + TopUpData(getCurrencyData(), selectedCurrency, getSelectedPaymentMethod(), bonusValue) + } + } + + override fun setNextButtonState(enabled: Boolean) { + button.isEnabled = enabled + } + + override fun paymentMethodsFocusRequest() { + hideKeyboard() + payment_methods.requestFocus() + selectedPaymentMethodId = adapter.getSelectedItemData().id + } + + override fun rotateChangeCurrencyButton() { + val rotateAnimation = RotateAnimation( + 0f, + 180f, + Animation.RELATIVE_TO_SELF, + 0.5f, + Animation.RELATIVE_TO_SELF, + 0.5f) + rotateAnimation.duration = 250 + rotateAnimation.interpolator = AccelerateDecelerateInterpolator() + swap_value_button.startAnimation(rotateAnimation) + } + + override fun switchCurrencyData() { + val currencyData = getCurrencyData() + selectedCurrency = + if (selectedCurrency == APPC_C_CURRENCY) FIAT_CURRENCY else APPC_C_CURRENCY + // We just have to switch the current information being shown + setupCurrencyData(selectedCurrency, currencyData.fiatCurrencyCode, currencyData.fiatValue, + currencyData.appcCode, currencyData.appcValue) + } + + override fun setConversionValue(topUpData: TopUpData) { + if (topUpData.selectedCurrencyType == selectedCurrency) { + when (selectedCurrency) { + FIAT_CURRENCY -> { + converted_value.text = "${topUpData.currency.appcValue} ${WalletCurrency.CREDITS.symbol}" + } + APPC_C_CURRENCY -> { + converted_value.text = + "${topUpData.currency.fiatValue} ${topUpData.currency.fiatCurrencyCode}" + } + } + } else { + when (selectedCurrency) { + FIAT_CURRENCY -> { + if (topUpData.currency.fiatValue != DEFAULT_VALUE) main_value.setText( + topUpData.currency.fiatValue) else main_value.setText("") + } + APPC_C_CURRENCY -> { + if (topUpData.currency.appcValue != DEFAULT_VALUE) main_value.setText( + topUpData.currency.appcValue) else main_value.setText("") + } + } + } + } + + override fun toggleSwitchCurrencyOn() { + switchingCurrency = true + } + + override fun toggleSwitchCurrencyOff() { + switchingCurrency = false + } + + override fun hideBonus() { + bonus_layout.visibility = View.INVISIBLE + bonus_msg.visibility = View.INVISIBLE + } + + override fun hideBonusAndSkeletons() { + hideBonus() + bonus_layout_skeleton.visibility = View.GONE + bonus_msg_skeleton.visibility = View.GONE + } + + override fun removeBonus() { + bonus_layout.visibility = View.GONE + bonus_msg.visibility = View.GONE + bonus_layout_skeleton.visibility = View.GONE + bonus_msg_skeleton.visibility = View.GONE + } + + override fun showBonus(bonus: BigDecimal, currency: String) { + buildBonusString(bonus, currency) + showBonus() + } + + private fun showBonus() { + bonus_layout_skeleton.visibility = View.GONE + bonus_msg_skeleton.visibility = View.GONE + bonus_msg.visibility = View.VISIBLE + bonus_layout.visibility = View.VISIBLE + } + + override fun showMaxValueWarning(value: String) { + value_warning_text.text = getString(R.string.topup_maximum_value, value) + value_warning_icon.visibility = View.VISIBLE + value_warning_text.visibility = View.VISIBLE + } + + override fun showMinValueWarning(value: String) { + value_warning_text.text = getString(R.string.topup_minimum_value, value) + value_warning_icon.visibility = View.VISIBLE + value_warning_text.visibility = View.VISIBLE + } + + override fun hideValueInputWarning() { + value_warning_icon.visibility = View.INVISIBLE + value_warning_text.visibility = View.INVISIBLE + } + + override fun changeMainValueColor(isValid: Boolean) { + if (isValid) { + main_value.setTextColor(ContextCompat.getColor(context!!, R.color.black)) + } else { + main_value.setTextColor(ContextCompat.getColor(context!!, R.color.color_grey_9e)) + } + } + + override fun changeMainValueText(value: String) { + main_value.setText(value) + main_value.setSelection(value.length) + } + + override fun getSelectedCurrency(): String { + return selectedCurrency + } + + override fun showNoNetworkError() { + hideKeyboard() + retry_animation.visibility = View.GONE + top_up_container.visibility = View.GONE + rv_default_values.visibility = View.GONE + no_network.visibility = View.VISIBLE + retry_button.visibility = View.VISIBLE + } + + override fun showRetryAnimation() { + retry_button.visibility = View.INVISIBLE + retry_animation.visibility = View.VISIBLE + } + + override fun retryClick() = RxView.clicks(retry_button) + + override fun showSkeletons() { + payments_skeleton.visibility = View.VISIBLE + bonus_layout_skeleton.visibility = View.VISIBLE + bonus_msg_skeleton.visibility = View.VISIBLE + } + + override fun showBonusSkeletons() { + bonus_msg.visibility = View.INVISIBLE + bonus_layout.visibility = View.INVISIBLE + bonus_layout_skeleton.visibility = View.VISIBLE + bonus_msg_skeleton.visibility = View.VISIBLE + } + + override fun hidePaymentMethods() { + payments_skeleton.visibility = View.VISIBLE + payment_methods.visibility = View.GONE + } + + private fun hideErrorViews() { + no_network.visibility = View.GONE + retry_button.visibility = View.GONE + retry_animation.visibility = View.GONE + top_up_container.visibility = View.VISIBLE + } + + private fun hideKeyboard() { + val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.hideSoftInputFromWindow(fragmentContainer.windowToken, 0) + } + + private fun buildBonusString(bonus: BigDecimal, bonusCurrency: String) { + val scaledBonus = bonus.max(BigDecimal("0.01")) + val currency = "~$bonusCurrency".takeIf { bonus < BigDecimal("0.01") } ?: bonusCurrency + bonusValue = scaledBonus + bonus_layout.bonus_header_1.text = getString(R.string.topup_bonus_header_part_1) + bonus_layout.bonus_value.text = getString(R.string.topup_bonus_header_part_2, + currency + formatter.formatCurrency(scaledBonus, WalletCurrency.FIAT)) + } + + private fun setupCurrencyData(selectedCurrency: String, fiatCode: String, fiatValue: String, + appcCode: String, appcValue: String) { + + when (selectedCurrency) { + FIAT_CURRENCY -> { + setCurrencyInfo(fiatCode, fiatValue, + "$appcValue $appcCode", appcCode) + } + APPC_C_CURRENCY -> { + setCurrencyInfo(appcCode, appcValue, + "$fiatValue $fiatCode", fiatCode) + } + } + } + + private fun setCurrencyInfo(mainCode: String, mainValue: String, + conversionValue: String, conversionCode: String) { + main_currency_code.text = mainCode + if (mainValue != DEFAULT_VALUE) { + main_value.setText(mainValue) + main_value.setSelection(main_value.text!!.length) + } + swap_value_label.text = conversionCode + converted_value.text = conversionValue + } + + private fun getSelectedPaymentMethod(): PaymentTypeInfo? { + return if (payment_methods.adapter != null) { + val data = (payment_methods.adapter as TopUpPaymentMethodsAdapter).getSelectedItemData() + when { + PaymentType.PAYPAL.subTypes.contains(data.id) -> + PaymentTypeInfo(PaymentType.PAYPAL, data.id, data.label, data.iconUrl) + PaymentType.CARD.subTypes.contains(data.id) -> + PaymentTypeInfo(PaymentType.CARD, data.id, data.label, data.iconUrl) + else -> PaymentTypeInfo(PaymentType.LOCAL_PAYMENTS, data.id, data.label, + data.iconUrl) + } + } else { + null + } + } + + private fun getCurrencyData(): CurrencyData { + return if (selectedCurrency == FIAT_CURRENCY) { + val appcValue = converted_value.text.toString() + .replace(APPC_C_SYMBOL, "") + .replace(" ", "") + val localCurrencyValue = + if (main_value.text.toString() + .isEmpty()) DEFAULT_VALUE else main_value.text.toString() + CurrencyData(localCurrency.code, localCurrency.symbol, localCurrencyValue, + APPC_C_SYMBOL, APPC_C_SYMBOL, appcValue) + } else { + val localCurrencyValue = converted_value.text.toString() + .replace(localCurrency.code, "") + .replace(" ", "") + val appcValue = + if (main_value.text.toString() + .isEmpty()) DEFAULT_VALUE else main_value.text.toString() + CurrencyData(localCurrency.code, localCurrency.symbol, localCurrencyValue, + APPC_C_SYMBOL, APPC_C_SYMBOL, appcValue) + } + } + + private fun isLocalCurrencyValid(localCurrency: LocalCurrency): Boolean { + return localCurrency.symbol != "" && localCurrency.code != "" + } + + private fun getAppBarHeight(): Int { + if (context == null) { + return 0 + } + return with(TypedValue().also { + context!!.theme.resolveAttribute(android.R.attr.actionBarSize, it, true) + }) { + TypedValue.complexToDimensionPixelSize(this.data, resources.displayMetrics) + } + } + + private fun dpToPx(value: Float) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, + Resources.getSystem().displayMetrics) + .toInt() + + private fun getTopUpValuesSpanCount(): Int { + val screenWidth = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, + fragmentContainer.measuredWidth.toFloat(), + requireContext().resources + .displayMetrics) + .toInt() + + val viewWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80f, + requireContext().resources + .displayMetrics) + .toInt() + + return screenWidth / viewWidth + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpFragmentPresenter.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpFragmentPresenter.kt new file mode 100644 index 00000000000..cd78e41049f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpFragmentPresenter.kt @@ -0,0 +1,382 @@ +package com.asfoundation.wallet.topup + +import android.os.Bundle +import android.util.Log +import com.asfoundation.wallet.billing.adyen.PaymentType +import com.asfoundation.wallet.topup.TopUpData.Companion.DEFAULT_VALUE +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.isNoNetworkException +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.BiFunction +import java.math.BigDecimal +import java.util.concurrent.TimeUnit + + +class TopUpFragmentPresenter(private val view: TopUpFragmentView, + private val activity: TopUpActivityView?, + private val interactor: TopUpInteractor, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val topUpAnalytics: TopUpAnalytics, + private val formatter: CurrencyFormatUtils, + private val selectedValue: String?) { + + private var cachedGamificationLevel = 0 + private var hasDefaultValues = false + + companion object { + private const val NUMERIC_REGEX = "^([1-9]|[0-9]+[,.]+[0-9])[0-9]*?\$" + private const val GAMIFICATION_LEVEL = "gamification_level" + } + + fun present(appPackage: String, savedInstanceState: Bundle?) { + savedInstanceState?.let { + cachedGamificationLevel = savedInstanceState.getInt(GAMIFICATION_LEVEL) + } + setupUi() + handleChangeCurrencyClick() + handleNextClick() + handleRetryClick() + handleManualAmountChange(appPackage) + handlePaymentMethodSelected() + handleValuesClicks() + handleKeyboardEvents() + } + + fun stop() { + interactor.cleanCachedValues() + disposables.dispose() + } + + private fun setupUi() { + disposables.add(Single.zip( + interactor.getLimitTopUpValues() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler), + interactor.getDefaultValues() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler), + BiFunction { values: TopUpLimitValues, defaultValues: TopUpValuesModel -> + if (values.error.hasError || defaultValues.error.hasError && + (values.error.isNoNetwork || defaultValues.error.isNoNetwork)) { + view.showNoNetworkError() + } else { + view.setupCurrency(LocalCurrency(values.maxValue.symbol, values.maxValue.currency)) + updateDefaultValues(defaultValues) + } + }) + .subscribe({}, { handleError(it) })) + } + + private fun retrievePaymentMethods(fiatAmount: String, currency: String): Completable { + return interactor.getPaymentMethods(fiatAmount, currency) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { + if (it.isNotEmpty()) view.setupPaymentMethods(it) + } + .ignoreElement() + } + + private fun updateDefaultValues(topUpValuesModel: TopUpValuesModel) { + hasDefaultValues = topUpValuesModel.error.hasError.not() && topUpValuesModel.values.size >= 3 + if (hasDefaultValues) { + val defaultValues = topUpValuesModel.values + val defaultFiatValue = defaultValues.drop(1) + .first() + view.setDefaultAmountValue(selectedValue ?: defaultFiatValue.amount.toString()) + view.setValuesAdapter(defaultValues) + } else { + view.hideValuesAdapter() + } + } + + private fun handleKeyboardEvents() { + disposables.add(view.getKeyboardEvents() + .doOnNext { + if (it && hasDefaultValues) view.showValuesAdapter() + else view.hideValuesAdapter() + } + .subscribeOn(viewScheduler) + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleError(throwable: Throwable) { + throwable.printStackTrace() + if (throwable.isNoNetworkException()) view.showNoNetworkError() + } + + private fun handleChangeCurrencyClick() { + disposables.add(view.getChangeCurrencyClick() + .doOnNext { + view.toggleSwitchCurrencyOn() + view.rotateChangeCurrencyButton() + view.switchCurrencyData() + view.toggleSwitchCurrencyOff() + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleNextClick() { + disposables.add(view.getNextClick() + .throttleFirst(500, TimeUnit.MILLISECONDS) + .observeOn(networkScheduler) + .switchMap { topUpData -> + interactor.getLimitTopUpValues() + .toObservable() + .filter { + isCurrencyValid(topUpData.currency) && isValueInRange(it, + topUpData.currency.fiatValue.toDouble()) && topUpData.paymentMethod != null + } + .observeOn(viewScheduler) + .doOnNext { + topUpAnalytics.sendSelectionEvent(topUpData.currency.appcValue.toDouble(), + "next", topUpData.paymentMethod!!.paymentType.name) + navigateToPayment(topUpData, cachedGamificationLevel) + } + } + .subscribe({}, { handleError(it) })) + } + + + private fun handleManualAmountChange(packageName: String) { + disposables.add(view.getEditTextChanges() + .doOnNext { resetValues(it) } + .debounce(700, TimeUnit.MILLISECONDS, viewScheduler) + .doOnNext { handleInputValue(it) } + .filter { isNumericOrEmpty(it) } + .switchMapCompletable { topUpData -> + getConvertedValue(topUpData) + .subscribeOn(networkScheduler) + .map { value -> updateConversionValue(value.amount, topUpData) } + .filter { isConvertedValueAvailable(it) } + .observeOn(viewScheduler) + .doOnComplete { view.setConversionValue(topUpData) } + .flatMapCompletable { + interactor.getLimitTopUpValues() + .toObservable() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .flatMapCompletable { handleInsertedValue(packageName, topUpData, it) } + } + .doOnError { handleError(it) } + .onErrorComplete() + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleInvalidFormatInput() { + handleEmptyOrDefaultInput() + view.hideValueInputWarning() + view.changeMainValueColor(false) + } + + private fun handleEmptyOrDefaultInput() { + view.hideBonus() + view.setNextButtonState(false) + } + + private fun resetValues(topUpData: TopUpData) { + view.setNextButtonState(false) + view.hideValueInputWarning() + updateConversionValue(BigDecimal.ZERO, topUpData) + view.setConversionValue(topUpData) + } + + private fun handleInputValue(topUpData: TopUpData) { + if (isNumericOrEmpty(topUpData)) { + if (topUpData.currency.fiatValue == DEFAULT_VALUE) { + handleEmptyOrDefaultInput() + } + } else { + handleInvalidFormatInput() + } + } + + private fun isNumericOrEmpty(data: TopUpData): Boolean { + return if (data.selectedCurrencyType == TopUpData.FIAT_CURRENCY) { + data.currency.fiatValue == DEFAULT_VALUE || data.currency.fiatValue.matches( + NUMERIC_REGEX.toRegex()) + } else { + data.currency.appcValue == DEFAULT_VALUE || data.currency.appcValue.matches( + NUMERIC_REGEX.toRegex()) + } + } + + private fun getConvertedValue(data: TopUpData): Observable { + return if (data.selectedCurrencyType == TopUpData.FIAT_CURRENCY + && data.currency.fiatValue != DEFAULT_VALUE) { + interactor.convertLocal(data.currency.fiatCurrencyCode, + data.currency.fiatValue, 2) + } else if (data.selectedCurrencyType == TopUpData.APPC_C_CURRENCY + && data.currency.appcValue != DEFAULT_VALUE) { + interactor.convertAppc(data.currency.appcValue) + } else { + Observable.just(FiatValue(BigDecimal.ZERO, "")) + } + } + + private fun handlePaymentMethodSelected() { + disposables.add(view.getPaymentMethodClick() + .doOnNext { view.paymentMethodsFocusRequest() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun loadBonusIntoView(appPackage: String, amount: String, + currency: String): Completable { + return interactor.convertLocal(currency, amount, 18) + .flatMapSingle { interactor.getEarningBonus(appPackage, it.amount) } + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnNext { + if (interactor.isBonusValidAndActive(it)) { + val scaledBonus = formatter.scaleFiat(it.amount) + view.showBonus(scaledBonus, it.currency) + } else { + view.removeBonus() + } + view.setNextButtonState(true) + cachedGamificationLevel = it.level + } + .ignoreElements() + } + + private fun handleInsertedValue(packageName: String, topUpData: TopUpData, + limitValues: TopUpLimitValues): Completable { + view.setNextButtonState(false) + val fiatAmount = BigDecimal(topUpData.currency.fiatValue) + if (topUpData.currency.fiatValue != DEFAULT_VALUE && !limitValues.error.hasError) { + handleValueWarning(limitValues.maxValue, limitValues.minValue, fiatAmount) + } else { + handleInvalidFormatInput() + } + return updateUiInformation(packageName, limitValues, + topUpData.currency.fiatValue, topUpData.currency.fiatCurrencyCode) + } + + private fun updateUiInformation(appPackage: String, + limitValues: TopUpLimitValues, fiatAmount: String, + currency: String): Completable { + return if (isValueInRange(limitValues, fiatAmount.toDouble())) { + view.changeMainValueColor(true) + view.hidePaymentMethods() + if (interactor.isBonusValidAndActive()) view.showBonusSkeletons() + retrievePaymentMethods(fiatAmount, currency) + .andThen(loadBonusIntoView(appPackage, fiatAmount, currency)) + } else { + view.hideBonusAndSkeletons() + view.changeMainValueColor(false) + view.setNextButtonState(false) + Completable.complete() + } + } + + private fun handleRetryClick() { + disposables.add(view.retryClick() + .observeOn(viewScheduler) + .doOnNext { view.showRetryAnimation() } + .delay(1, TimeUnit.SECONDS) + .observeOn(viewScheduler) + .doOnNext { + view.showSkeletons() + setupUi() + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleValueWarning(maxValue: FiatValue, minValue: FiatValue, amount: BigDecimal) { + val localCurrency = " ${maxValue.currency}" + when { + amount == BigDecimal(-1) -> { + view.hideValueInputWarning() + Log.w("TopUpFragmentPresenter", "Unable to retrieve values") + } + amount > maxValue.amount -> view.showMaxValueWarning( + maxValue.amount.toPlainString() + localCurrency) + amount < minValue.amount -> view.showMinValueWarning( + minValue.amount.toPlainString() + localCurrency) + else -> view.hideValueInputWarning() + } + } + + private fun isValueInRange(limitValues: TopUpLimitValues, value: Double): Boolean { + return limitValues.error.hasError || limitValues.minValue.amount.toDouble() <= value && + limitValues.maxValue.amount.toDouble() >= value + } + + private fun isCurrencyValid(currencyData: CurrencyData): Boolean { + return currencyData.appcValue != DEFAULT_VALUE && currencyData.fiatValue != DEFAULT_VALUE + } + + private fun updateConversionValue(value: BigDecimal, topUpData: TopUpData): TopUpData { + if (topUpData.selectedCurrencyType == TopUpData.FIAT_CURRENCY) { + topUpData.currency.appcValue = + if (value == BigDecimal.ZERO) DEFAULT_VALUE else value.toString() + } else { + topUpData.currency.fiatValue = + if (value == BigDecimal.ZERO) DEFAULT_VALUE else value.toString() + } + return topUpData + } + + private fun isConvertedValueAvailable(data: TopUpData): Boolean { + return if (data.selectedCurrencyType == TopUpData.FIAT_CURRENCY) { + data.currency.appcValue != DEFAULT_VALUE + } else { + data.currency.fiatValue != DEFAULT_VALUE + } + } + + private fun handleValuesClicks() { + disposables.add(view.getValuesClicks() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .doOnNext { view.disableSwapCurrencyButton() } + .doOnNext { + if (view.getSelectedCurrency() == TopUpData.FIAT_CURRENCY) { + view.changeMainValueText(it.amount.toString()) + } else { + convertAndChangeMainValue(it.currency, it.amount) + } + } + .debounce(300, TimeUnit.MILLISECONDS, viewScheduler) + .doOnNext { view.enableSwapCurrencyButton() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun convertAndChangeMainValue(currency: String, amount: BigDecimal) { + disposables.add(interactor.convertLocal(currency, amount.toString(), 2) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnNext { view.changeMainValueText(it.amount.toString()) } + .doOnError { handleError(it) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun navigateToPayment(topUpData: TopUpData, gamificationLevel: Int) { + val paymentMethod = topUpData.paymentMethod!! + when (paymentMethod.paymentType) { + PaymentType.CARD, PaymentType.PAYPAL -> activity?.navigateToAdyenPayment( + paymentMethod.paymentType, mapTopUpPaymentData(topUpData, gamificationLevel)) + PaymentType.LOCAL_PAYMENTS -> + activity?.navigateToLocalPayment(paymentMethod.paymentId, paymentMethod.icon, + paymentMethod.label, mapTopUpPaymentData(topUpData, gamificationLevel)) + } + } + + private fun mapTopUpPaymentData(topUpData: TopUpData, gamificationLevel: Int): TopUpPaymentData { + return TopUpPaymentData(topUpData.currency.fiatValue, topUpData.currency.fiatCurrencyCode, + topUpData.selectedCurrencyType, topUpData.bonusValue, topUpData.currency.fiatCurrencySymbol, + topUpData.currency.appcValue, "TOPUP", gamificationLevel) + } + + fun onSavedInstance(outState: Bundle) { + outState.putInt(GAMIFICATION_LEVEL, cachedGamificationLevel) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpFragmentView.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpFragmentView.kt new file mode 100644 index 00000000000..2900459a376 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpFragmentView.kt @@ -0,0 +1,47 @@ +package com.asfoundation.wallet.topup + +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.ui.iab.PaymentMethod +import io.reactivex.Observable +import java.math.BigDecimal + +interface TopUpFragmentView { + + fun getChangeCurrencyClick(): Observable + fun getEditTextChanges(): Observable + fun getPaymentMethodClick(): Observable + fun getNextClick(): Observable + fun setupPaymentMethods(paymentMethods: List) + fun setupCurrency(localCurrency: LocalCurrency) + fun setConversionValue(topUpData: TopUpData) + fun switchCurrencyData() + fun setNextButtonState(enabled: Boolean) + fun rotateChangeCurrencyButton() + fun toggleSwitchCurrencyOn() + fun toggleSwitchCurrencyOff() + fun hideBonus() + fun hideBonusAndSkeletons() + fun showBonus(bonus: BigDecimal, currency: String) + fun showMaxValueWarning(value: String) + fun showMinValueWarning(value: String) + fun hideValueInputWarning() + fun changeMainValueColor(isValid: Boolean) + fun changeMainValueText(value: String) + fun getSelectedCurrency(): String + fun paymentMethodsFocusRequest() + fun disableSwapCurrencyButton() + fun enableSwapCurrencyButton() + fun showNoNetworkError() + fun showRetryAnimation() + fun retryClick(): Observable + fun getValuesClicks(): Observable + fun setValuesAdapter(values: List) + fun showValuesAdapter() + fun hideValuesAdapter() + fun getKeyboardEvents(): Observable + fun setDefaultAmountValue(amount: String) + fun removeBonus() + fun showSkeletons() + fun showBonusSkeletons() + fun hidePaymentMethods() +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpInteractor.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpInteractor.kt new file mode 100644 index 00000000000..ce11c7c1b3e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpInteractor.kt @@ -0,0 +1,133 @@ +package com.asfoundation.wallet.topup + +import com.appcoins.wallet.bdsbilling.repository.BdsRepository +import com.appcoins.wallet.bdsbilling.repository.entity.FeeEntity +import com.appcoins.wallet.bdsbilling.repository.entity.FeeType +import com.appcoins.wallet.bdsbilling.repository.entity.PaymentMethodEntity +import com.appcoins.wallet.gamification.repository.ForecastBonusAndLevel +import com.asfoundation.wallet.backup.NotificationNeeded +import com.asfoundation.wallet.service.LocalCurrencyConversionService +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.ui.gamification.GamificationInteractor +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor +import com.asfoundation.wallet.ui.iab.PaymentMethod +import com.asfoundation.wallet.ui.iab.PaymentMethodFee +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +import java.math.BigDecimal +import java.util.* +import kotlin.collections.ArrayList + +class TopUpInteractor(private val repository: BdsRepository, + private val conversionService: LocalCurrencyConversionService, + private val gamificationInteractor: GamificationInteractor, + private val topUpValuesService: TopUpValuesService, + private val chipValueIndexMap: LinkedHashMap, + private var limitValues: TopUpLimitValues, + private var walletBlockedInteract: WalletBlockedInteract, + private var inAppPurchaseInteractor: InAppPurchaseInteractor, + private var supportInteractor: SupportInteractor) { + + + fun getPaymentMethods(value: String, currency: String): Single> { + return repository.getPaymentMethods(value, currency, "fiat", true) + .map { mapPaymentMethods(it) } + } + + fun isWalletBlocked() = walletBlockedInteract.isWalletBlocked() + + fun incrementAndValidateNotificationNeeded(): Single = + inAppPurchaseInteractor.incrementAndValidateNotificationNeeded() + + fun showSupport(): Completable { + return gamificationInteractor.getUserStats() + .map { it.level } + .onErrorReturn { 0 } + .flatMapCompletable { level -> + inAppPurchaseInteractor.walletAddress + .flatMapCompletable { wallet -> + Completable.fromAction { + supportInteractor.registerUser(level, wallet.toLowerCase(Locale.ROOT)) + supportInteractor.displayChatScreen() + } + } + } + } + + fun convertAppc(value: String): Observable { + return conversionService.getAppcToLocalFiat(value, 2) + } + + fun convertLocal(currency: String, value: String, scale: Int): Observable { + return conversionService.getLocalToAppc(currency, value, scale) + } + + private fun mapPaymentMethods( + paymentMethods: List): List { + return paymentMethods.map { + PaymentMethod(it.id, it.label, it.iconUrl, + mapPaymentMethodFee(it.fee), it.isAvailable(), null) + } + } + + private fun mapPaymentMethodFee(feeEntity: FeeEntity?): PaymentMethodFee? { + return feeEntity?.let { + if (feeEntity.type === FeeType.EXACT) { + PaymentMethodFee(true, feeEntity.cost?.value, feeEntity.cost?.currency) + } else { + PaymentMethodFee(false, null, null) + } + } + } + + fun getEarningBonus(packageName: String, amount: BigDecimal): Single { + return gamificationInteractor.getEarningBonus(packageName, amount) + } + + fun getLimitTopUpValues(): Single { + return if (limitValues.maxValue != TopUpLimitValues.INITIAL_LIMIT_VALUE && + limitValues.minValue != TopUpLimitValues.INITIAL_LIMIT_VALUE) { + Single.just(limitValues) + } else { + topUpValuesService.getLimitValues() + .doOnSuccess { if (!it.error.hasError) cacheLimitValues(it) } + } + } + + fun getDefaultValues(): Single { + return if (chipValueIndexMap.isNotEmpty()) { + Single.just(TopUpValuesModel(ArrayList(chipValueIndexMap.keys))) + } else { + topUpValuesService.getDefaultValues() + .doOnSuccess { if (!it.error.hasError) cacheChipValues(it.values) } + } + } + + fun cleanCachedValues() { + limitValues = TopUpLimitValues() + chipValueIndexMap.clear() + } + + fun isBonusValidAndActive(): Boolean { + return gamificationInteractor.isBonusActiveAndValid() + } + + fun isBonusValidAndActive(forecastBonusAndLevel: ForecastBonusAndLevel): Boolean { + return gamificationInteractor.isBonusActiveAndValid(forecastBonusAndLevel) + } + + private fun cacheChipValues(chipValues: List) { + for (index in chipValues.indices) { + chipValueIndexMap[chipValues[index]] = index + } + } + + private fun cacheLimitValues(values: TopUpLimitValues) { + limitValues = TopUpLimitValues(values.minValue, values.maxValue) + } + + fun getWalletAddress(): Single = inAppPurchaseInteractor.walletAddress +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpItemDecorator.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpItemDecorator.kt new file mode 100644 index 00000000000..390d7f552d7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpItemDecorator.kt @@ -0,0 +1,48 @@ +package com.asfoundation.wallet.topup + +import android.graphics.Rect +import android.util.TypedValue +import android.view.View +import androidx.recyclerview.widget.RecyclerView + + +class TopUpItemDecorator(private val size: Int, private val addMargin: Boolean) : + RecyclerView.ItemDecoration() { + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, + state: RecyclerView.State) { + if (addMargin) { + val position: Int = parent.getChildAdapterPosition(view) + val spanCount = size + + val screenWidth = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, parent.measuredWidth.toFloat(), + parent.context.resources + .displayMetrics) + .toInt() + + val viewWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80f, + parent.context.resources + .displayMetrics) + .toInt() + + val spacing = (((screenWidth - viewWidth * spanCount) / (spanCount + 1)) * 0.99).toInt() + + when { + position == 0 -> { + outRect.left = spacing + outRect.right = spacing / 2 + } + position < (parent.adapter?.itemCount ?: 0) - 1 -> { + outRect.left = spacing / 2 + outRect.right = spacing / 2 + } + else -> { + outRect.left = spacing / 2 + outRect.right = spacing + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpLimitValues.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpLimitValues.kt new file mode 100644 index 00000000000..4370e62a2d2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpLimitValues.kt @@ -0,0 +1,16 @@ +package com.asfoundation.wallet.topup + +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.util.Error +import java.math.BigDecimal + +data class TopUpLimitValues(val minValue: FiatValue = INITIAL_LIMIT_VALUE, + val maxValue: FiatValue = INITIAL_LIMIT_VALUE, + val error: Error = Error()) { + + constructor(isNoNetwork: Boolean) : this(error = Error(true, isNoNetwork)) + + companion object { + val INITIAL_LIMIT_VALUE = FiatValue(BigDecimal(-1), "", "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpLimitValuesResponse.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpLimitValuesResponse.kt new file mode 100644 index 00000000000..af358a8f94c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpLimitValuesResponse.kt @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.topup + + +data class TopUpLimitValuesResponse(val currency: Currency, val values: Values) { + + data class Currency(val code: String, val sign: String) + data class Values(val min: String, val max: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpPaymentData.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpPaymentData.kt new file mode 100644 index 00000000000..a4ba5686de6 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpPaymentData.kt @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.topup + +import java.io.Serializable +import java.math.BigDecimal + +data class TopUpPaymentData(val fiatValue: String, val fiatCurrencyCode: String, + val selectedCurrencyType: String, + val bonusValue: BigDecimal, val fiatCurrencySymbol: String, + val appcValue: String, val transactionType: String, + val gamificationLevel: Int) : Serializable diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpSuccessFragment.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpSuccessFragment.kt new file mode 100644 index 00000000000..0f81085eee9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpSuccessFragment.kt @@ -0,0 +1,180 @@ +package com.asfoundation.wallet.topup + +import android.content.Context +import android.graphics.Typeface +import android.os.Bundle +import android.text.Spannable +import android.text.SpannableString +import android.text.style.StyleSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.lottie.FontAssetDelegate +import com.airbnb.lottie.TextDelegate +import com.asf.wallet.R +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import kotlinx.android.synthetic.main.fragment_top_up_success.* +import javax.inject.Inject + +class TopUpSuccessFragment : DaggerFragment(), TopUpSuccessFragmentView { + + companion object { + @JvmStatic + fun newInstance(amount: String, currency: String, bonus: String, + currencySymbol: String): TopUpSuccessFragment { + return TopUpSuccessFragment().apply { + arguments = Bundle().apply { + putString(PARAM_AMOUNT, amount) + putString(CURRENCY, currency) + putString(CURRENCY_SYMBOL, currencySymbol) + putString(BONUS, bonus) + } + } + } + + private const val PARAM_AMOUNT = "amount" + private const val CURRENCY = "currency" + private const val CURRENCY_SYMBOL = "currency_symbol" + private const val BONUS = "bonus" + } + + @Inject + lateinit var formatter: CurrencyFormatUtils + private lateinit var presenter: TopUpSuccessPresenter + private lateinit var topUpActivityView: TopUpActivityView + + val amount: String? by lazy { + if (arguments!!.containsKey(PARAM_AMOUNT)) { + arguments!!.getString(PARAM_AMOUNT) + } else { + throw IllegalArgumentException("product name not found") + } + } + + val currency: String? by lazy { + if (arguments!!.containsKey(CURRENCY)) { + arguments!!.getString(CURRENCY) + } else { + throw IllegalArgumentException("currency not found") + } + } + + val bonus: String by lazy { + if (arguments!!.containsKey(BONUS)) { + arguments!!.getString(BONUS, "") + } else { + throw IllegalArgumentException("bonus not found") + } + } + + private val currencySymbol: String by lazy { + if (arguments!!.containsKey(CURRENCY_SYMBOL)) { + arguments!!.getString(CURRENCY_SYMBOL, "") + } else { + throw IllegalArgumentException("bonus not found") + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is TopUpActivityView) { + throw IllegalStateException( + "Express checkout buy fragment must be attached to IAB activity") + } + topUpActivityView = context + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = TopUpSuccessPresenter(this) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_top_up_success, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + presenter.present() + topUpActivityView.showToolbar() + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + override fun show() { + if (bonus.isNotEmpty()) { + top_up_success_animation.setAnimation(R.raw.top_up_bonus_success_animation) + setAnimationText() + formatBonusSuccessMessage() + } else { + top_up_success_animation.setAnimation(R.raw.top_up_success_animation) + formatSuccessMessage() + } + top_up_success_animation.playAnimation() + } + + override fun clean() { + top_up_success_animation.removeAllAnimatorListeners() + top_up_success_animation.removeAllUpdateListeners() + top_up_success_animation.removeAllLottieOnCompositionLoadedListener() + } + + override fun close() { + topUpActivityView.close() + } + + override fun getOKClicks(): Observable { + return RxView.clicks(button) + } + + private fun setAnimationText() { + val formattedBonus = formatter.formatCurrency(bonus, WalletCurrency.FIAT) + val textDelegate = TextDelegate(top_up_success_animation) + textDelegate.setText("bonus_value", "$currencySymbol$formattedBonus") + textDelegate.setText("bonus_received", + resources.getString(R.string.gamification_purchase_completed_bonus_received)) + top_up_success_animation.setTextDelegate(textDelegate) + top_up_success_animation.setFontAssetDelegate(object : FontAssetDelegate() { + override fun fetchFont(fontFamily: String?): Typeface { + return Typeface.create("sans-serif-medium", Typeface.BOLD) + } + }) + } + + private fun formatBonusSuccessMessage() { + val formattedInitialString = getFormattedTopUpValue() + val topUpString = + formattedInitialString + " " + resources.getString(R.string.topup_completed_2_with_bonus) + setSpannableString(topUpString, formattedInitialString.length) + + } + + private fun formatSuccessMessage() { + val formattedInitialString = getFormattedTopUpValue() + val secondStringFormat = + String.format(resources.getString(R.string.askafriend_notification_received_body), + formattedInitialString, "\n") + setSpannableString(secondStringFormat, formattedInitialString.length) + } + + private fun getFormattedTopUpValue(): String { + val fiatValue = + formatter.formatCurrency(amount!!, WalletCurrency.FIAT) + " " + currency + return String.format(resources.getString(R.string.topup_completed_1), fiatValue) + } + + private fun setSpannableString(secondStringFormat: String, firstStringLength: Int) { + val boldStyle = StyleSpan(Typeface.BOLD) + val sb = SpannableString(secondStringFormat) + sb.setSpan(boldStyle, 0, firstStringLength, Spannable.SPAN_INCLUSIVE_INCLUSIVE) + value.text = sb + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpSuccessFragmentView.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpSuccessFragmentView.kt new file mode 100644 index 00000000000..e23bd7a700c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpSuccessFragmentView.kt @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.topup + +import io.reactivex.Observable + +interface TopUpSuccessFragmentView { + + fun show() + + fun clean() + + fun close() + + fun getOKClicks(): Observable +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpSuccessPresenter.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpSuccessPresenter.kt new file mode 100644 index 00000000000..5332b52ab7a --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpSuccessPresenter.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.topup + +import io.reactivex.disposables.CompositeDisposable + +class TopUpSuccessPresenter(private val view: TopUpSuccessFragmentView) { + private val disposables: CompositeDisposable = CompositeDisposable() + + fun present() { + view.show() + + handleOKClick() + } + + fun stop() { + disposables.clear() + view.clean() + } + + private fun handleOKClick() { + disposables.add( + view.getOKClicks().doOnNext { + view.close() + }.subscribe()) + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpValuesApiResponseMapper.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpValuesApiResponseMapper.kt new file mode 100644 index 00000000000..2104c06f652 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpValuesApiResponseMapper.kt @@ -0,0 +1,23 @@ +package com.asfoundation.wallet.topup + +import com.asfoundation.wallet.ui.iab.FiatValue +import java.math.BigDecimal + + +class TopUpValuesApiResponseMapper { + + fun map(defaultValues: TopUpDefaultValuesResponse): TopUpValuesModel { + return TopUpValuesModel(ArrayList(defaultValues.items.map { + FiatValue(BigDecimal(it.price.fiat.value), it.price.fiat.currency.code, + it.price.fiat.currency.sign) + })) + } + + fun mapValues(limitValuesResponse: TopUpLimitValuesResponse): TopUpLimitValues { + return TopUpLimitValues( + FiatValue(BigDecimal(limitValuesResponse.values.min), limitValuesResponse.currency.code, + limitValuesResponse.currency.sign), + FiatValue(BigDecimal(limitValuesResponse.values.max), limitValuesResponse.currency.code, + limitValuesResponse.currency.sign)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpValuesModel.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpValuesModel.kt new file mode 100644 index 00000000000..3b33b591403 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpValuesModel.kt @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.topup + +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.util.Error + +data class TopUpValuesModel(val values: List, val error: Error = Error()) { + constructor(isNoNetworkError: Boolean) : this(listOf(FiatValue()), Error(true, isNoNetworkError)) +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/TopUpValuesService.kt b/app/src/main/java/com/asfoundation/wallet/topup/TopUpValuesService.kt new file mode 100644 index 00000000000..1b122d25e09 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/TopUpValuesService.kt @@ -0,0 +1,37 @@ +package com.asfoundation.wallet.topup + +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.util.isNoNetworkException +import io.reactivex.Single +import retrofit2.http.GET +import retrofit2.http.Path + +class TopUpValuesService(private val api: TopUpValuesApi, + private val responseMapper: TopUpValuesApiResponseMapper) { + + fun getDefaultValues(): Single { + return api.getDefaultValues(BuildConfig.APPLICATION_ID) + .map { responseMapper.map(it) } + .onErrorReturn { createErrorValuesList(it) } + } + + fun getLimitValues(): Single { + return api.getInputLimitValues(BuildConfig.APPLICATION_ID) + .map { responseMapper.mapValues(it) } + .onErrorReturn { TopUpLimitValues(it.isNoNetworkException()) } + } + + private fun createErrorValuesList(throwable: Throwable): TopUpValuesModel { + return TopUpValuesModel(throwable.isNoNetworkException()) + } + + interface TopUpValuesApi { + @GET("product/8.20180518/topup/billing/domains/{packageName}") + fun getInputLimitValues(@Path("packageName") + packageName: String): Single + + @GET("product/8.20200402/topup/billing/domains/{packageName}/skus") + fun getDefaultValues( + @Path("packageName") packageName: String): Single + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/address/BillingAddressTopUpFragment.kt b/app/src/main/java/com/asfoundation/wallet/topup/address/BillingAddressTopUpFragment.kt new file mode 100644 index 00000000000..2b6497a8467 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/address/BillingAddressTopUpFragment.kt @@ -0,0 +1,271 @@ +package com.asfoundation.wallet.topup.address + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.widget.ArrayAdapter +import com.asf.wallet.R +import com.asfoundation.wallet.billing.address.BillingAddressModel +import com.asfoundation.wallet.billing.address.BillingAddressTextWatcher +import com.asfoundation.wallet.topup.TopUpActivity.Companion.BILLING_ADDRESS_CANCEL_CODE +import com.asfoundation.wallet.topup.TopUpActivity.Companion.BILLING_ADDRESS_REQUEST_CODE +import com.asfoundation.wallet.topup.TopUpActivity.Companion.BILLING_ADDRESS_SUCCESS_CODE +import com.asfoundation.wallet.topup.TopUpActivityView +import com.asfoundation.wallet.topup.TopUpData +import com.asfoundation.wallet.topup.TopUpPaymentData +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.fragment_billing_address_top_up.* +import kotlinx.android.synthetic.main.layout_billing_address.* +import kotlinx.android.synthetic.main.view_purchase_bonus.view.* +import java.math.BigDecimal +import javax.inject.Inject + +class BillingAddressTopUpFragment : DaggerFragment(), BillingAddressTopUpView { + + companion object { + + const val BILLING_ADDRESS_MODEL = "billing_address_model" + private const val PAYMENT_DATA = "data" + private const val FIAT_AMOUNT_KEY = "fiat_amount" + private const val FIAT_CURRENCY_KEY = "fiat_currency" + private const val STORE_CARD_KEY = "store_card" + private const val IS_STORED_KEY = "is_stored" + + @JvmStatic + fun newInstance(data: TopUpPaymentData, fiatAmount: String, fiatCurrency: String, + shouldStoreCard: Boolean, isStored: Boolean): BillingAddressTopUpFragment { + return BillingAddressTopUpFragment().apply { + arguments = Bundle().apply { + putSerializable(PAYMENT_DATA, data) + putString(FIAT_AMOUNT_KEY, fiatAmount) + putString(FIAT_CURRENCY_KEY, fiatCurrency) + putBoolean(STORE_CARD_KEY, shouldStoreCard) + putBoolean(IS_STORED_KEY, isStored) + } + } + } + } + + @Inject + lateinit var formatter: CurrencyFormatUtils + + private lateinit var topUpView: TopUpActivityView + private lateinit var presenter: BillingAddressTopUpPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = + BillingAddressTopUpPresenter(this, CompositeDisposable(), AndroidSchedulers.mainThread()) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_billing_address_top_up, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupUi() + presenter.present() + } + + private fun setupUi() { + showBonus() + showValues() + setupFieldsListener() + setupStateAdapter() + button.setText(R.string.topup_home_button) + if (isStored) remember.visibility = View.GONE + else { + remember.visibility = VISIBLE + remember.isChecked = shouldStoreCard + } + } + + private fun setupFieldsListener() { + address.addTextChangedListener(BillingAddressTextWatcher(address_layout)) + number.addTextChangedListener(BillingAddressTextWatcher(number_layout)) + city.addTextChangedListener(BillingAddressTextWatcher(city_layout)) + zipcode.addTextChangedListener(BillingAddressTextWatcher(zipcode_layout)) + state.addTextChangedListener(BillingAddressTextWatcher(state_layout)) + } + + private fun setupStateAdapter() { + val languages = resources.getStringArray(R.array.states) + val adapter = ArrayAdapter(requireContext(), R.layout.item_billing_address_state, languages) + state.setAdapter(adapter) + } + + override fun finishSuccess(billingAddressModel: BillingAddressModel) { + val intent = Intent().apply { + putExtra(BILLING_ADDRESS_MODEL, billingAddressModel) + } + targetFragment?.onActivityResult(BILLING_ADDRESS_REQUEST_CODE, BILLING_ADDRESS_SUCCESS_CODE, + intent) + topUpView.navigateBack() + } + + override fun cancel() { + targetFragment?.onActivityResult(BILLING_ADDRESS_REQUEST_CODE, BILLING_ADDRESS_CANCEL_CODE, + null) + topUpView.navigateBack() + } + + override fun submitClicks(): Observable { + return RxView.clicks(button) + .filter { validateFields() } + .map { + BillingAddressModel( + address.text.toString(), + city.text.toString(), + zipcode.text.toString(), + state.text.toString(), + country.text.toString(), + number.text.toString(), + remember.isChecked + ) + } + } + + private fun validateFields(): Boolean { + var valid = true + if (address.text.isNullOrEmpty()) { + valid = false + address_layout.error = getString(R.string.error_field_required) + } + + if (number.text.isNullOrEmpty()) { + valid = false + number_layout.error = getString(R.string.error_field_required) + } + + if (city.text.isNullOrEmpty()) { + valid = false + city_layout.error = getString(R.string.error_field_required) + } + + if (zipcode.text.isNullOrEmpty()) { + valid = false + zipcode_layout.error = getString(R.string.error_field_required) + } + + if (state.text.isNullOrEmpty()) { + valid = false + state_layout.error = getString(R.string.error_field_required) + } + + if (country.text.isNullOrEmpty()) { + valid = false + country_layout.error = getString(R.string.error_field_required) + } + + return valid + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check( + context is TopUpActivityView) { "billing address fragment must be attached to TopUp activity" } + topUpView = context + } + + private fun showBonus() { + if (data.bonusValue.compareTo(BigDecimal.ZERO) != 0) { + bonus_layout?.visibility = VISIBLE + bonus_msg?.visibility = VISIBLE + val scaledBonus = data.bonusValue.max(BigDecimal("0.01")) + val currency = "~${data.fiatCurrencySymbol}".takeIf { data.bonusValue < BigDecimal("0.01") } + ?: data.fiatCurrencySymbol + bonus_layout?.bonus_header_1?.text = getString(R.string.topup_bonus_header_part_1) + bonus_layout?.bonus_value?.text = getString(R.string.topup_bonus_header_part_2, + currency + formatter.formatCurrency(scaledBonus, WalletCurrency.FIAT)) + } + } + + private fun showValues() { + main_value.visibility = VISIBLE + val formattedValue = formatter.formatCurrency(data.appcValue, WalletCurrency.CREDITS) + if (data.selectedCurrencyType == TopUpData.FIAT_CURRENCY) { + main_value.setText(fiatAmount) + main_currency_code.text = fiatCurrency + converted_value.text = "$formattedValue ${WalletCurrency.CREDITS.symbol}" + } else { + main_value.setText(formattedValue) + main_currency_code.text = WalletCurrency.CREDITS.symbol + converted_value.text = "$fiatAmount $fiatCurrency" + } + } + + override fun showLoading() { + topUpView.lockOrientation() + loading.visibility = VISIBLE + billing_info_container.visibility = View.INVISIBLE + title.visibility = View.INVISIBLE + button.isEnabled = false + } + + override fun hideLoading() { + topUpView.unlockRotation() + button.visibility = VISIBLE + loading.visibility = View.GONE + button.isEnabled = true + title.visibility = VISIBLE + billing_info_container.visibility = VISIBLE + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + private val fiatAmount: String by lazy { + if (arguments!!.containsKey(FIAT_AMOUNT_KEY)) { + arguments!!.getString(FIAT_AMOUNT_KEY, "") + } else { + throw IllegalArgumentException("fiat amount data not found") + } + } + + private val fiatCurrency: String by lazy { + if (arguments!!.containsKey(FIAT_CURRENCY_KEY)) { + arguments!!.getString(FIAT_CURRENCY_KEY, "") + } else { + throw IllegalArgumentException("fiat currency data not found") + } + } + + private val data: TopUpPaymentData by lazy { + if (arguments!!.containsKey(PAYMENT_DATA)) { + arguments!!.getSerializable(PAYMENT_DATA) as TopUpPaymentData + } else { + throw IllegalArgumentException("previous payment data not found") + } + } + + private val shouldStoreCard: Boolean by lazy { + if (arguments!!.containsKey(STORE_CARD_KEY)) { + arguments!!.getBoolean(STORE_CARD_KEY) + } else { + throw IllegalArgumentException("should store card data not found") + } + } + + private val isStored: Boolean by lazy { + if (arguments!!.containsKey(IS_STORED_KEY)) { + arguments!!.getBoolean(IS_STORED_KEY) + } else { + throw IllegalArgumentException("is stored data not found") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/address/BillingAddressTopUpPresenter.kt b/app/src/main/java/com/asfoundation/wallet/topup/address/BillingAddressTopUpPresenter.kt new file mode 100644 index 00000000000..5d148320901 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/address/BillingAddressTopUpPresenter.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.topup.address + +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +class BillingAddressTopUpPresenter( + private val view: BillingAddressTopUpView, + private val disposables: CompositeDisposable, + private val viewScheduler: Scheduler) { + + fun present() { + handleSubmitClicks() + } + + private fun handleSubmitClicks() { + disposables.add( + view.submitClicks() + .subscribeOn(viewScheduler) + .doOnNext { view.finishSuccess(it) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + fun stop() = disposables.clear() + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/address/BillingAddressTopUpView.kt b/app/src/main/java/com/asfoundation/wallet/topup/address/BillingAddressTopUpView.kt new file mode 100644 index 00000000000..c8804d4a00d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/address/BillingAddressTopUpView.kt @@ -0,0 +1,18 @@ +package com.asfoundation.wallet.topup.address + +import com.asfoundation.wallet.billing.address.BillingAddressModel +import io.reactivex.Observable + +interface BillingAddressTopUpView { + + fun submitClicks(): Observable + + fun showLoading() + + fun hideLoading() + + fun finishSuccess(billingAddressModel: BillingAddressModel) + + fun cancel() + +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/payment/AdyenTopUpFragment.kt b/app/src/main/java/com/asfoundation/wallet/topup/payment/AdyenTopUpFragment.kt new file mode 100644 index 00000000000..b58bddb0a2a --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/payment/AdyenTopUpFragment.kt @@ -0,0 +1,568 @@ +package com.asfoundation.wallet.topup.payment + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.* +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.annotation.StringRes +import androidx.appcompat.widget.SwitchCompat +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.lifecycle.Observer +import com.adyen.checkout.adyen3ds2.Adyen3DS2Component +import com.adyen.checkout.base.model.paymentmethods.StoredPaymentMethod +import com.adyen.checkout.base.model.payments.response.Action +import com.adyen.checkout.base.ui.view.RoundCornerImageView +import com.adyen.checkout.card.CardComponent +import com.adyen.checkout.card.CardConfiguration +import com.adyen.checkout.core.api.Environment +import com.adyen.checkout.redirect.RedirectComponent +import com.appcoins.wallet.bdsbilling.Billing +import com.asf.wallet.BuildConfig +import com.asf.wallet.R +import com.asfoundation.wallet.billing.address.BillingAddressModel +import com.asfoundation.wallet.billing.adyen.* +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.navigator.UriNavigator +import com.asfoundation.wallet.service.ServicesErrorCodeMapper +import com.asfoundation.wallet.topup.TopUpActivity.Companion.BILLING_ADDRESS_REQUEST_CODE +import com.asfoundation.wallet.topup.TopUpActivity.Companion.BILLING_ADDRESS_SUCCESS_CODE +import com.asfoundation.wallet.topup.TopUpActivityView +import com.asfoundation.wallet.topup.TopUpAnalytics +import com.asfoundation.wallet.topup.TopUpData.Companion.FIAT_CURRENCY +import com.asfoundation.wallet.topup.TopUpPaymentData +import com.asfoundation.wallet.topup.address.BillingAddressTopUpFragment.Companion.BILLING_ADDRESS_MODEL +import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.KeyboardUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.google.android.material.textfield.TextInputLayout +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.ReplaySubject +import kotlinx.android.synthetic.main.adyen_credit_card_pre_selected.* +import kotlinx.android.synthetic.main.error_top_up_layout.* +import kotlinx.android.synthetic.main.fragment_adyen_top_up.* +import kotlinx.android.synthetic.main.no_network_retry_only_layout.* +import kotlinx.android.synthetic.main.selected_payment_method_cc.* +import kotlinx.android.synthetic.main.support_error_layout.layout_support_icn +import kotlinx.android.synthetic.main.support_error_layout.layout_support_logo +import kotlinx.android.synthetic.main.support_error_layout.view.* +import kotlinx.android.synthetic.main.view_purchase_bonus.view.* +import java.math.BigDecimal +import javax.inject.Inject + +class AdyenTopUpFragment : DaggerFragment(), AdyenTopUpView { + + @Inject + internal lateinit var inAppPurchaseInteractor: InAppPurchaseInteractor + + @Inject + internal lateinit var billing: Billing + + @Inject + lateinit var adyenPaymentInteractor: AdyenPaymentInteractor + + @Inject + lateinit var adyenEnvironment: Environment + + @Inject + lateinit var topUpAnalytics: TopUpAnalytics + + @Inject + lateinit var formatter: CurrencyFormatUtils + + @Inject + lateinit var servicesErrorMapper: ServicesErrorCodeMapper + + @Inject + lateinit var logger: Logger + + private lateinit var topUpView: TopUpActivityView + private lateinit var cardConfiguration: CardConfiguration + private lateinit var redirectComponent: RedirectComponent + private lateinit var adyen3DS2Component: Adyen3DS2Component + private lateinit var adyenCardNumberLayout: TextInputLayout + private lateinit var adyenExpiryDateLayout: TextInputLayout + private lateinit var adyenSecurityCodeLayout: TextInputLayout + private lateinit var navigator: PaymentFragmentNavigator + private lateinit var presenter: AdyenTopUpPresenter + + private var adyenCardImageLayout: RoundCornerImageView? = null + private var adyenSaveDetailsSwitch: SwitchCompat? = null + private var paymentDataSubject: ReplaySubject? = null + private var paymentDetailsSubject: PublishSubject? = null + private var adyen3DSErrorSubject: PublishSubject? = null + private var billingAddressInput: PublishSubject? = null + private var isStored = false + private var billingAddressModel: BillingAddressModel? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + paymentDataSubject = ReplaySubject.createWithSize(1) + paymentDetailsSubject = PublishSubject.create() + adyen3DSErrorSubject = PublishSubject.create() + billingAddressInput = PublishSubject.create() + + presenter = + AdyenTopUpPresenter(this, appPackage, AndroidSchedulers.mainThread(), Schedulers.io(), + CompositeDisposable(), RedirectComponent.getReturnUrl(context!!), paymentType, + data.transactionType, data.fiatValue, data.fiatCurrencyCode, data.appcValue, + data.selectedCurrencyType, navigator, inAppPurchaseInteractor.billingMessagesMapper, + adyenPaymentInteractor, data.bonusValue, data.fiatCurrencySymbol, + AdyenErrorCodeMapper(), servicesErrorMapper, data.gamificationLevel, topUpAnalytics, + formatter, logger) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check( + context is TopUpActivityView) { "Payment Auth fragment must be attached to TopUp activity" } + topUpView = context + navigator = PaymentFragmentNavigator((activity as UriNavigator?)!!, topUpView) + + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present(savedInstanceState) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_adyen_top_up, container, false) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + if (this::adyenCardNumberLayout.isInitialized) { + outState.apply { + putString(CARD_NUMBER_KEY, adyenCardNumberLayout.editText?.text.toString()) + putString(EXPIRY_DATE_KEY, adyenExpiryDateLayout.editText?.text.toString()) + putString(CVV_KEY, adyenSecurityCodeLayout.editText?.text.toString()) + putBoolean(SAVE_DETAILS_KEY, adyenSaveDetailsSwitch?.isChecked ?: false) + } + } + presenter.onSaveInstanceState(outState) + } + + override fun onResume() { + super.onResume() + hideKeyboard() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == BILLING_ADDRESS_REQUEST_CODE && resultCode == BILLING_ADDRESS_SUCCESS_CODE) { + val billingAddressModel = + data!!.getSerializableExtra(BILLING_ADDRESS_MODEL) as BillingAddressModel + this.billingAddressModel = billingAddressModel + billingAddressInput?.onNext(true) + } + } + + override fun setup3DSComponent() { + adyen3DS2Component = Adyen3DS2Component.PROVIDER.get(this) + adyen3DS2Component.observe(this, Observer { + paymentDetailsSubject?.onNext(AdyenComponentResponseModel(it.details, it.paymentData)) + }) + adyen3DS2Component.observeErrors(this, Observer { + adyen3DSErrorSubject?.onNext(it.errorMessage) + }) + } + + override fun showValues(value: String, currency: String) { + main_value.visibility = VISIBLE + val formattedValue = formatter.formatCurrency(data.appcValue, WalletCurrency.CREDITS) + if (data.selectedCurrencyType == FIAT_CURRENCY) { + main_value.setText(value) + main_currency_code.text = currency + converted_value.text = "$formattedValue ${WalletCurrency.CREDITS.symbol}" + } else { + main_value.setText(formattedValue) + main_currency_code.text = WalletCurrency.CREDITS.symbol + converted_value.text = "$value $currency" + } + } + + override fun showLoading() { + loading.visibility = VISIBLE + credit_card_info_container.visibility = INVISIBLE + button.isEnabled = false + } + + override fun hideLoading() { + button.visibility = VISIBLE + loading.visibility = GONE + button.isEnabled = false + credit_card_info_container.visibility = VISIBLE + } + + override fun showNetworkError() { + topUpView.unlockRotation() + loading.visibility = GONE + no_network.visibility = VISIBLE + retry_button.visibility = VISIBLE + retry_animation.visibility = GONE + top_up_container.visibility = GONE + } + + override fun showRetryAnimation() { + retry_button.visibility = INVISIBLE + retry_animation.visibility = VISIBLE + } + + override fun hideErrorViews() { + no_network.visibility = GONE + top_up_container.visibility = VISIBLE + main_currency_code.visibility = VISIBLE + main_value.visibility = VISIBLE + top_separator_topup.visibility = VISIBLE + converted_value.visibility = VISIBLE + button.visibility = VISIBLE + + if (isStored) { + change_card_button.visibility = VISIBLE + } else { + change_card_button.visibility = GONE + } + + credit_card_info_container.visibility = VISIBLE + fragment_adyen_error?.visibility = GONE + + topUpView.unlockRotation() + } + + override fun showSpecificError(@StringRes stringRes: Int) { + topUpView.unlockRotation() + viewModelStore.clear() + loading.visibility = GONE + top_up_container.visibility = GONE + + val message = getString(stringRes) + fragment_adyen_error?.error_message?.text = message + fragment_adyen_error?.visibility = VISIBLE + + } + + override fun showCvvError() { + topUpView.unlockRotation() + loading.visibility = GONE + button.isEnabled = false + if (isStored) { + change_card_button.visibility = VISIBLE + } else { + change_card_button.visibility = INVISIBLE + } + credit_card_info_container.visibility = VISIBLE + + adyenSecurityCodeLayout.error = getString(R.string.purchase_card_error_CVV) + } + + override fun retryClick() = RxView.clicks(retry_button) + + override fun getTryAgainClicks() = RxView.clicks(try_again) + + override fun getSupportClicks(): Observable { + return Observable.merge(RxView.clicks(layout_support_logo), RxView.clicks(layout_support_icn)) + } + + override fun topUpButtonClicked() = RxView.clicks(button) + + override fun billingAddressInput(): Observable { + return billingAddressInput!! + } + + override fun retrieveBillingAddressData() = billingAddressModel + + override fun navigateToPaymentSelection() { + topUpView.navigateBack() + } + + override fun navigateToBillingAddress(fiatAmount: String, fiatCurrency: String) { + topUpView.unlockRotation() + topUpView.navigateToBillingAddress(data, fiatAmount, fiatCurrency, this, + adyenSaveDetailsSwitch?.isChecked ?: true, isStored) + } + + override fun finishCardConfiguration( + paymentMethod: com.adyen.checkout.base.model.paymentmethods.PaymentMethod, + isStored: Boolean, forget: Boolean, savedInstanceState: Bundle?) { + this.isStored = isStored + handleLayoutVisibility(isStored) + prepareCardComponent(paymentMethod, forget, savedInstanceState) + setStoredPaymentInformation(isStored) + } + + override fun lockRotation() = topUpView.lockOrientation() + + private fun prepareCardComponent( + paymentMethodEntity: com.adyen.checkout.base.model.paymentmethods.PaymentMethod, + forget: Boolean, + savedInstanceState: Bundle?) { + if (forget) viewModelStore.clear() + val cardComponent = + CardComponent.PROVIDER.get(this, paymentMethodEntity, cardConfiguration) + if (forget) clearFields() + adyen_card_form_pre_selected?.attach(cardComponent, this) + cardComponent.observe(this, androidx.lifecycle.Observer { + if (it != null && it.isValid) { + button.isEnabled = true + view?.let { view -> KeyboardUtils.hideKeyboard(view) } + it.data.paymentMethod?.let { paymentMethod -> + val hasCvc = !paymentMethod.encryptedSecurityCode.isNullOrEmpty() + val supportedShopperInteractions = + if (paymentMethodEntity is StoredPaymentMethod) paymentMethodEntity.supportedShopperInteractions else emptyList() + paymentDataSubject?.onNext( + AdyenCardWrapper(paymentMethod, adyenSaveDetailsSwitch?.isChecked ?: false, hasCvc, + supportedShopperInteractions)) + } + } else { + button.isEnabled = false + } + }) + if (!forget) { + getFieldValues(savedInstanceState) + } + } + + private fun handleLayoutVisibility(isStored: Boolean) { + if (isStored) { + adyenCardNumberLayout.visibility = GONE + adyenExpiryDateLayout.visibility = GONE + adyenCardImageLayout?.visibility = GONE + change_card_button?.visibility = VISIBLE + change_card_button_pre_selected?.visibility = VISIBLE + } else { + adyenCardNumberLayout.visibility = VISIBLE + adyenExpiryDateLayout.visibility = VISIBLE + adyenCardImageLayout?.visibility = VISIBLE + change_card_button?.visibility = GONE + change_card_button_pre_selected?.visibility = GONE + } + } + + override fun setupRedirectComponent() { + redirectComponent = RedirectComponent.PROVIDER.get(this) + redirectComponent.observe(this, Observer { + paymentDetailsSubject?.onNext(AdyenComponentResponseModel(it.details, it.paymentData)) + }) + } + + override fun handle3DSAction(action: Action) { + adyen3DS2Component.handleAction(activity!!, action) + } + + override fun onAdyen3DSError(): Observable = adyen3DSErrorSubject!! + + override fun showBonus(bonus: BigDecimal, currency: String) { + buildBonusString(bonus, currency) + bonus_layout.visibility = VISIBLE + bonus_msg.visibility = VISIBLE + } + + override fun showWalletValidation(@StringRes error: Int) = topUpView.showWalletValidation(error) + + private fun buildBonusString(bonus: BigDecimal, bonusCurrency: String) { + val scaledBonus = bonus.max(BigDecimal("0.01")) + val currency = "~$bonusCurrency".takeIf { bonus < BigDecimal("0.01") } ?: bonusCurrency + bonus_layout.bonus_header_1.text = getString(R.string.topup_bonus_header_part_1) + bonus_layout.bonus_value.text = getString(R.string.topup_bonus_header_part_2, + currency + formatter.formatCurrency(scaledBonus, WalletCurrency.FIAT)) + } + + override fun retrievePaymentData() = paymentDataSubject!! + + override fun getPaymentDetails() = paymentDetailsSubject!! + + override fun forgetCardClick(): Observable { + return if (change_card_button != null) RxView.clicks(change_card_button) + else RxView.clicks(change_card_button_pre_selected) + } + + override fun submitUriResult(uri: Uri) = redirectComponent.handleRedirectResponse(uri) + + override fun updateTopUpButton(valid: Boolean) { + button.isEnabled = valid + } + + override fun cancelPayment() = topUpView.cancelPayment() + + override fun setFinishingPurchase() = topUpView.setFinishingPurchase() + + private fun setStoredPaymentInformation(isStored: Boolean) { + if (isStored) { + adyen_card_form_pre_selected_number?.text = + adyenCardNumberLayout.editText?.text + adyen_card_form_pre_selected_number?.visibility = VISIBLE + payment_method_ic?.setImageDrawable(adyenCardImageLayout?.drawable) + view?.let { KeyboardUtils.showKeyboard(it) } + } else { + adyen_card_form_pre_selected_number?.visibility = GONE + payment_method_ic?.visibility = GONE + } + } + + private fun getFieldValues(savedInstanceState: Bundle?) { + savedInstanceState?.let { + adyenCardNumberLayout.editText?.setText(it.getString(CARD_NUMBER_KEY, "")) + adyenExpiryDateLayout.editText?.setText(it.getString(EXPIRY_DATE_KEY, "")) + adyenSecurityCodeLayout.editText?.setText(it.getString(CVV_KEY, "")) + adyenSaveDetailsSwitch?.isChecked = it.getBoolean(SAVE_DETAILS_KEY, false) + it.clear() + } + } + + private fun clearFields() { + adyenCardNumberLayout.editText?.text = null + adyenCardNumberLayout.editText?.isEnabled = true + adyenExpiryDateLayout.editText?.text = null + adyenExpiryDateLayout.editText?.isEnabled = true + adyenSecurityCodeLayout.editText?.text = null + adyenCardNumberLayout.requestFocus() + adyenSecurityCodeLayout.error = null + } + + override fun setupUi() { + credit_card_info_container.visibility = INVISIBLE + button.isEnabled = false + + if (paymentType == PaymentType.CARD.name) { + button.setText(R.string.topup_home_button) + + setupAdyenLayouts() + setupCardConfiguration() + } + + topUpView.showToolbar() + main_value.visibility = INVISIBLE + button.visibility = VISIBLE + } + + private fun setupCardConfiguration() { + val cardConfigurationBuilder = + CardConfiguration.Builder(activity as Context, BuildConfig.ADYEN_PUBLIC_KEY) + + cardConfiguration = cardConfigurationBuilder.let { + it.setEnvironment(adyenEnvironment) + it.build() + } + } + + private fun setupAdyenLayouts() { + adyenCardNumberLayout = + adyen_card_form_pre_selected?.findViewById(R.id.textInputLayout_cardNumber) + ?: adyen_card_form.findViewById(R.id.textInputLayout_cardNumber) + adyenExpiryDateLayout = + adyen_card_form_pre_selected?.findViewById(R.id.textInputLayout_expiryDate) + ?: adyen_card_form.findViewById(R.id.textInputLayout_expiryDate) + adyenSecurityCodeLayout = + adyen_card_form_pre_selected?.findViewById(R.id.textInputLayout_securityCode) + ?: adyen_card_form.findViewById(R.id.textInputLayout_securityCode) + adyenCardImageLayout = adyen_card_form_pre_selected?.findViewById(R.id.cardBrandLogo_imageView) + ?: adyen_card_form?.findViewById(R.id.cardBrandLogo_imageView) + adyenSaveDetailsSwitch = + adyen_card_form_pre_selected?.findViewById(R.id.switch_storePaymentMethod) + ?: adyen_card_form?.findViewById(R.id.switch_storePaymentMethod) + + adyenSaveDetailsSwitch?.run { + + val params: LinearLayout.LayoutParams = this.layoutParams as LinearLayout.LayoutParams + params.topMargin = 4 + params.bottomMargin = 0 + + layoutParams = params + isChecked = true + textSize = 14f + text = getString(R.string.dialog_credit_card_remember) + setPadding(0, 0, 0, 0) + } + + val height = resources.getDimensionPixelSize(R.dimen.adyen_text_input_layout_height) + + val view: View = adyen_card_form_pre_selected ?: adyen_card_form + val layoutParams: ConstraintLayout.LayoutParams = + view.layoutParams as ConstraintLayout.LayoutParams + layoutParams.bottomMargin = 0 + layoutParams.marginStart = 0 + layoutParams.marginEnd = 0 + layoutParams.topMargin = 0 + view.layoutParams = layoutParams + view.setPadding(0, 0, 24, 0) + + adyenCardNumberLayout.minimumHeight = height + adyenExpiryDateLayout.minimumHeight = height + adyenSecurityCodeLayout.minimumHeight = height + } + + override fun hideKeyboard() { + view?.let { KeyboardUtils.hideKeyboard(it) } + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + override fun onDestroy() { + hideKeyboard() + billingAddressInput = null + paymentDataSubject = null + paymentDetailsSubject = null + adyen3DSErrorSubject = null + super.onDestroy() + } + + private val appPackage: String by lazy { + if (activity != null) { + activity!!.packageName + } else { + throw IllegalArgumentException("previous app package name not found") + } + } + + private val data: TopUpPaymentData by lazy { + if (arguments!!.containsKey(PAYMENT_DATA)) { + arguments!!.getSerializable(PAYMENT_DATA) as TopUpPaymentData + } else { + throw IllegalArgumentException("previous payment data not found") + } + } + + private val paymentType: String by lazy { + if (arguments!!.containsKey(PAYMENT_TYPE)) { + arguments!!.getString(PAYMENT_TYPE)!! + } else { + throw IllegalArgumentException("Payment Type not found") + } + } + + companion object { + + private const val PAYMENT_TYPE = "paymentType" + private const val PAYMENT_DATA = "data" + private const val CARD_NUMBER_KEY = "card_number" + private const val EXPIRY_DATE_KEY = "expiry_date" + private const val CVV_KEY = "cvv_key" + private const val SAVE_DETAILS_KEY = "save_details" + + fun newInstance(paymentType: PaymentType, data: TopUpPaymentData): AdyenTopUpFragment { + val bundle = Bundle() + val fragment = AdyenTopUpFragment() + bundle.apply { + putString(PAYMENT_TYPE, paymentType.name) + putSerializable(PAYMENT_DATA, data) + fragment.arguments = this + } + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/payment/AdyenTopUpPresenter.kt b/app/src/main/java/com/asfoundation/wallet/topup/payment/AdyenTopUpPresenter.kt new file mode 100644 index 00000000000..aa84f927f13 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/payment/AdyenTopUpPresenter.kt @@ -0,0 +1,541 @@ +package com.asfoundation.wallet.topup.payment + +import android.os.Bundle +import androidx.annotation.StringRes +import com.adyen.checkout.base.model.paymentmethods.PaymentMethod +import com.appcoins.wallet.billing.BillingMessagesMapper +import com.appcoins.wallet.billing.adyen.AdyenBillingAddress +import com.appcoins.wallet.billing.adyen.AdyenPaymentRepository +import com.appcoins.wallet.billing.adyen.AdyenResponseMapper.Companion.REDIRECT +import com.appcoins.wallet.billing.adyen.AdyenResponseMapper.Companion.THREEDS2CHALLENGE +import com.appcoins.wallet.billing.adyen.AdyenResponseMapper.Companion.THREEDS2FINGERPRINT +import com.appcoins.wallet.billing.adyen.PaymentModel +import com.appcoins.wallet.billing.adyen.TransactionResponse.Status +import com.appcoins.wallet.billing.adyen.TransactionResponse.Status.* +import com.appcoins.wallet.billing.util.Error +import com.asf.wallet.R +import com.asfoundation.wallet.billing.address.BillingAddressModel +import com.asfoundation.wallet.billing.adyen.AdyenErrorCodeMapper +import com.asfoundation.wallet.billing.adyen.AdyenErrorCodeMapper.Companion.CVC_DECLINED +import com.asfoundation.wallet.billing.adyen.AdyenErrorCodeMapper.Companion.FRAUD +import com.asfoundation.wallet.billing.adyen.AdyenPaymentInteractor +import com.asfoundation.wallet.billing.adyen.PaymentType +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.service.ServicesErrorCodeMapper +import com.asfoundation.wallet.topup.TopUpAnalytics +import com.asfoundation.wallet.topup.TopUpData +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.ui.iab.Navigator +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import java.math.BigDecimal +import java.util.concurrent.TimeUnit + +class AdyenTopUpPresenter(private val view: AdyenTopUpView, + private val appPackage: String, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val returnUrl: String, + private val paymentType: String, + private val transactionType: String, + private val amount: String, + private val currency: String, + private val appcValue: String, + private val selectedCurrency: String, + private val navigator: Navigator, + private val billingMessagesMapper: BillingMessagesMapper, + private val adyenPaymentInteractor: AdyenPaymentInteractor, + private val bonusValue: BigDecimal, + private val fiatCurrencySymbol: String, + private val adyenErrorCodeMapper: AdyenErrorCodeMapper, + private val servicesErrorMapper: ServicesErrorCodeMapper, + private val gamificationLevel: Int, + private val topUpAnalytics: TopUpAnalytics, + private val formatter: CurrencyFormatUtils, + private val logger: Logger) { + + private var waitingResult = false + private var currentError: Int = 0 + private var cachedUid = "" + private var cachedPaymentData: String? = null + private var retrievedAmount = amount + private var retrievedCurrency = currency + + fun present(savedInstanceState: Bundle?) { + view.setupUi() + view.showLoading() + retrieveSavedInstance(savedInstanceState) + view.setup3DSComponent() + view.setupRedirectComponent() + handleViewState(savedInstanceState) + handleForgetCardClick() + handleRetryClick(savedInstanceState) + handleRedirectResponse() + handleSupportClicks() + handleTryAgainClicks() + handleAdyen3DSErrors() + handlePaymentDetails() + } + + private fun handleViewState(savedInstanceState: Bundle?) { + if (currentError != 0) { + view.showSpecificError(currentError) + if (paymentType == PaymentType.CARD.name) loadPaymentMethodInfo(savedInstanceState) + } else { + if (waitingResult) view.showLoading() + else loadPaymentMethodInfo(savedInstanceState) + } + } + + private fun loadBonusIntoView() { + if (bonusValue.compareTo(BigDecimal.ZERO) != 0) { + view.showBonus(bonusValue, fiatCurrencySymbol) + } + } + + private fun handleRetryClick(savedInstanceState: Bundle?) { + disposables.add(view.retryClick() + .observeOn(viewScheduler) + .doOnNext { view.showRetryAnimation() } + .delay(1, TimeUnit.SECONDS) + .doOnNext { + if (waitingResult) { + view.navigateToPaymentSelection() + } else { + loadPaymentMethodInfo(savedInstanceState, true) + } + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleSupportClicks() { + disposables.add(view.getSupportClicks() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .flatMapCompletable { adyenPaymentInteractor.showSupport(gamificationLevel) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleTryAgainClicks() { + disposables.add(view.getTryAgainClicks() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .doOnNext { + if (paymentType == PaymentType.CARD.name) hideSpecificError() + else view.navigateToPaymentSelection() + } + .subscribeOn(viewScheduler) + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun loadPaymentMethodInfo(savedInstanceState: Bundle?, fromError: Boolean = false) { + disposables.add(convertAmount() + .flatMap { + adyenPaymentInteractor.loadPaymentInfo(mapPaymentToService(paymentType), it.toString(), + currency) + } + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { + view.hideLoading() + if (fromError) view.hideErrorViews() + if (it.error.hasError) { + if (it.error.isNetworkError) view.showNetworkError() + else handleSpecificError(R.string.unknown_error) + } else { + val priceAmount = formatter.formatCurrency(it.priceAmount, WalletCurrency.FIAT) + view.showValues(priceAmount, it.priceCurrency) + retrievedAmount = it.priceAmount.toString() + retrievedCurrency = it.priceCurrency + if (paymentType == PaymentType.CARD.name) { + view.finishCardConfiguration(it.paymentMethodInfo!!, it.isStored, false, + savedInstanceState) + handleTopUpClick() + } else if (paymentType == PaymentType.PAYPAL.name) { + launchPaypal(it.paymentMethodInfo!!) + } + loadBonusIntoView() + } + } + .subscribe({}, { handleSpecificError(R.string.unknown_error, it) })) + } + + private fun launchPaypal(paymentMethodInfo: PaymentMethod) { + disposables.add( + adyenPaymentInteractor.makeTopUpPayment(paymentMethodInfo, false, false, emptyList(), + returnUrl, retrievedAmount, retrievedCurrency, + mapPaymentToService(paymentType).transactionType, transactionType, appPackage) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .filter { !waitingResult } + .doOnSuccess { handlePaymentModel(it) } + .subscribe({}, { handleSpecificError(R.string.unknown_error, it) })) + } + + //Called if is card + private fun handleTopUpClick() { + disposables.add(Observable.merge(view.topUpButtonClicked(), view.billingAddressInput()) + .flatMapSingle { + view.retrievePaymentData() + .firstOrError() + } + .doOnNext { + view.showLoading() + view.lockRotation() + view.setFinishingPurchase() + } + .observeOn(networkScheduler) + .flatMapSingle { + val billingAddressModel = view.retrieveBillingAddressData() + val shouldStore = billingAddressModel?.remember ?: it.shouldStoreCard + topUpAnalytics.sendConfirmationEvent(appcValue.toDouble(), "top_up", paymentType) + adyenPaymentInteractor.makeTopUpPayment(it.cardPaymentMethod, shouldStore, + it.hasCvc, it.supportedShopperInteractions, returnUrl, retrievedAmount, + retrievedCurrency, mapPaymentToService(paymentType).transactionType, + transactionType, + appPackage, mapToAdyenBillingAddress(billingAddressModel)) + } + .observeOn(viewScheduler) + .flatMapCompletable { + if (it.action != null) { + Completable.fromAction { handlePaymentModel(it) } + } else { + handlePaymentResult(it) + } + } + .subscribe({}, { + view.showSpecificError(R.string.unknown_error) + logger.log(TAG, it) + })) + } + + private fun handleForgetCardClick() { + disposables.add(view.forgetCardClick() + .observeOn(viewScheduler) + .doOnNext { view.showLoading() } + .observeOn(networkScheduler) + .flatMapSingle { adyenPaymentInteractor.disablePayments() } + .observeOn(viewScheduler) + .doOnNext { success -> + if (!success) { + handleSpecificError(R.string.unknown_error, logMessage = "Unable to forget card") + } + } + .filter { it } + .observeOn(networkScheduler) + .flatMapSingle { + adyenPaymentInteractor.loadPaymentInfo(mapPaymentToService(paymentType), + amount, currency) + .observeOn(viewScheduler) + .doOnSuccess { + view.hideLoading() + if (it.error.hasError) { + if (it.error.isNetworkError) view.showNetworkError() + else { + handleSpecificError(R.string.unknown_error, + logMessage = "Message: ${it.error.message}, code: ${it.error.code}") + } + } else { + view.finishCardConfiguration(it.paymentMethodInfo!!, it.isStored, true, null) + } + } + } + .subscribe({}, { handleSpecificError(R.string.unknown_error, it) })) + } + + private fun handleRedirectResponse() { + disposables.add(navigator.uriResults() + .doOnNext { + topUpAnalytics.sendPaypalUrlEvent(appcValue.toDouble(), paymentType, + it.getQueryParameter("type"), it.getQueryParameter("resultCode"), it.toString()) + } + .observeOn(viewScheduler) + .doOnNext { view.submitUriResult(it) } + .subscribe({}, { handleSpecificError(R.string.unknown_error, it) })) + } + + //Called if is paypal or 3DS + private fun handlePaymentDetails() { + disposables.add(view.getPaymentDetails() + .observeOn(viewScheduler) + .doOnNext { + view.lockRotation() + view.hideKeyboard() + view.setFinishingPurchase() + } + .throttleLast(2, TimeUnit.SECONDS) + .observeOn(networkScheduler) + .flatMapSingle { + adyenPaymentInteractor.submitRedirect(cachedUid, it.details!!, + it.paymentData ?: cachedPaymentData) + } + .observeOn(viewScheduler) + .flatMapCompletable { handlePaymentResult(it) } + .subscribe({}, { handleSpecificError(R.string.unknown_error, it) })) + } + + private fun handlePaymentResult(paymentModel: PaymentModel): Completable { + return when { + paymentModel.resultCode.equals("AUTHORISED", ignoreCase = true) -> { + adyenPaymentInteractor.getAuthorisedTransaction(paymentModel.uid) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .flatMapCompletable { + if (it.status == COMPLETED) { + handleSuccessTransaction() + } else { + if (paymentModel.status == FAILED && paymentType == PaymentType.PAYPAL.name) { + retrieveFailedReason(paymentModel.uid) + } else { + Completable.fromAction { handleErrors(paymentModel, appcValue.toDouble()) } + } + } + } + } + paymentModel.status == PENDING_USER_PAYMENT && paymentModel.action != null -> { + Completable.fromAction { + view.showLoading() + view.lockRotation() + handleAdyenAction(paymentModel) + } + } + paymentModel.refusalReason != null -> Completable.fromAction { + topUpAnalytics.sendErrorEvent(appcValue.toDouble(), paymentType, "error", + paymentModel.refusalCode.toString(), paymentModel.refusalReason ?: "") + paymentModel.refusalCode?.let { code -> + when (code) { + CVC_DECLINED -> view.showCvvError() + FRAUD -> handleFraudFlow(adyenErrorCodeMapper.map(code)) + else -> handleSpecificError(adyenErrorCodeMapper.map(code)) + } + } + } + paymentModel.error.hasError -> Completable.fromAction { + if (isBillingAddressError(paymentModel.error)) { + view.navigateToBillingAddress(retrievedAmount, retrievedCurrency) + } else { + handleErrors(paymentModel, appcValue.toDouble()) + } + } + paymentModel.status == CANCELED -> Completable.fromAction { + topUpAnalytics.sendErrorEvent(appcValue.toDouble(), paymentType, "error", "", + "canceled") + view.cancelPayment() + } + paymentModel.status == FAILED && paymentType == PaymentType.PAYPAL.name -> { + retrieveFailedReason(paymentModel.uid) + } + else -> Completable.fromAction { + topUpAnalytics.sendErrorEvent(appcValue.toDouble(), paymentType, "error", + paymentModel.refusalCode.toString(), "${paymentModel.status}: Generic Error") + handleSpecificError(R.string.unknown_error) + } + } + } + + private fun retrieveFailedReason(uid: String): Completable { + return adyenPaymentInteractor.getFailedTransactionReason(uid) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .flatMapCompletable { + Completable.fromAction { + topUpAnalytics.sendErrorEvent(appcValue.toDouble(), paymentType, "error", + it.errorCode.toString(), it.errorMessage ?: "") + val message = if (it.errorCode != null) adyenErrorCodeMapper.map(it.errorCode!!) + else R.string.unknown_error + handleSpecificError(message) + } + } + } + + private fun isBillingAddressError(error: Error): Boolean { + return error.code != null + && error.code == 400 + && error.message?.contains("payment.billing_address") == true + } + + private fun handleSuccessTransaction(): Completable { + return Completable.fromAction { + topUpAnalytics.sendSuccessEvent(appcValue.toDouble(), paymentType, "success") + val bundle = createBundle(retrievedAmount, retrievedCurrency, fiatCurrencySymbol) + waitingResult = false + navigator.popView(bundle) + } + } + + private fun handleFraudFlow(@StringRes error: Int) { + disposables.add( + adyenPaymentInteractor.isWalletBlocked() + .subscribeOn(networkScheduler) + .observeOn(networkScheduler) + .flatMap { blocked -> + if (blocked) { + adyenPaymentInteractor.isWalletVerified() + .observeOn(viewScheduler) + .doOnSuccess { + if (it) handleSpecificError(error) + else view.showWalletValidation(error) + } + } else { + Single.just(true) + .observeOn(viewScheduler) + .doOnSuccess { handleSpecificError(error) } + } + } + .observeOn(viewScheduler) + .subscribe({}, { handleSpecificError(error, it) }) + ) + } + + private fun handleAdyen3DSErrors() { + disposables.add(view.onAdyen3DSError() + .observeOn(viewScheduler) + .doOnNext { + if (it == CHALLENGE_CANCELED) view.navigateToPaymentSelection() + else handleSpecificError(R.string.unknown_error, logMessage = it) + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun buildRefusalReason(status: Status, message: String?): String { + return message?.let { "$status : $it" } ?: status.toString() + } + + private fun handlePaymentModel(paymentModel: PaymentModel) { + if (paymentModel.error.hasError) { + handleErrors(paymentModel, appcValue.toDouble()) + } else { + view.showLoading() + view.lockRotation() + handleAdyenAction(paymentModel) + } + } + + private fun convertAmount(): Single { + return if (selectedCurrency == TopUpData.FIAT_CURRENCY) { + Single.just(BigDecimal(amount)) + } else adyenPaymentInteractor.convertToLocalFiat(BigDecimal(appcValue).toDouble()) + .map(FiatValue::amount) + } + + private fun createBundle(priceAmount: String, priceCurrency: String, + fiatCurrencySymbol: String): Bundle { + return billingMessagesMapper.topUpBundle(priceAmount, priceCurrency, bonusValue.toPlainString(), + fiatCurrencySymbol) + } + + private fun mapPaymentToService(paymentType: String): AdyenPaymentRepository.Methods { + return if (paymentType == PaymentType.CARD.name) { + AdyenPaymentRepository.Methods.CREDIT_CARD + } else { + AdyenPaymentRepository.Methods.PAYPAL + } + } + + private fun mapToAdyenBillingAddress( + billingAddressModel: BillingAddressModel?): AdyenBillingAddress? { + return billingAddressModel?.let { + AdyenBillingAddress(it.address, it.city, it.zipcode, it.number, it.state, it.country) + } + } + + fun stop() = disposables.clear() + + fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(WAITING_RESULT, waitingResult) + outState.putInt(CURRENT_ERROR, currentError) + outState.putString(UID, cachedUid) + outState.putString(RETRIEVED_AMOUNT, retrievedAmount) + outState.putString(RETRIEVED_CURRENCY, retrievedCurrency) + outState.putString(PAYMENT_DATA, cachedPaymentData) + } + + private fun retrieveSavedInstance(savedInstanceState: Bundle?) { + savedInstanceState?.let { + waitingResult = it.getBoolean(WAITING_RESULT) + currentError = it.getInt(CURRENT_ERROR) + cachedUid = it.getString(UID, "") + cachedPaymentData = it.getString(PAYMENT_DATA) + retrievedAmount = it.getString(RETRIEVED_AMOUNT, amount) + retrievedCurrency = it.getString(RETRIEVED_CURRENCY, currency) + } + } + + private fun handleSpecificError(@StringRes message: Int, throwable: Throwable? = null, + logMessage: String? = null) { + if (throwable != null) logger.log(TAG, throwable) + if (logMessage != null) logger.log(TAG, logMessage) + currentError = message + waitingResult = false + view.showSpecificError(message) + } + + private fun hideSpecificError() { + currentError = 0 + view.hideErrorViews() + } + + private fun handleAdyenAction(paymentModel: PaymentModel) { + if (paymentModel.action != null) { + val type = paymentModel.action?.type + if (type == REDIRECT) { + cachedPaymentData = paymentModel.paymentData + cachedUid = paymentModel.uid + navigator.navigateToUriForResult(paymentModel.redirectUrl) + waitingResult = true + } else if (type == THREEDS2FINGERPRINT || type == THREEDS2CHALLENGE) { + cachedUid = paymentModel.uid + view.handle3DSAction(paymentModel.action!!) + waitingResult = true + } else { + handleSpecificError(R.string.unknown_error, logMessage = "Unknown adyen action: $type") + } + } + } + + private fun handleErrors(paymentModel: PaymentModel, value: Double) { + when { + paymentModel.error.isNetworkError -> { + topUpAnalytics.sendErrorEvent(value, paymentType, "error", + paymentModel.error.code.toString(), + "network_error") + view.showNetworkError() + } + paymentModel.error.code != null -> { + topUpAnalytics.sendErrorEvent(value, paymentType, "error", + paymentModel.error.code.toString(), + buildRefusalReason(paymentModel.status, paymentModel.error.message)) + val resId = servicesErrorMapper.mapError(paymentModel.error.code!!) + if (paymentModel.error.code == HTTP_FRAUD_CODE) handleFraudFlow(resId) + else view.showSpecificError(resId) + } + else -> { + topUpAnalytics.sendErrorEvent(value, paymentType, "error", + paymentModel.error.code.toString(), + buildRefusalReason(paymentModel.status, paymentModel.error.message)) + handleSpecificError(R.string.unknown_error) + } + } + } + + companion object { + private const val WAITING_RESULT = "WAITING_RESULT" + private const val CURRENT_ERROR = "current_error" + private const val RETRIEVED_AMOUNT = "RETRIEVED_AMOUNT" + private const val RETRIEVED_CURRENCY = "RETRIEVED_CURRENCY" + private const val UID = "UID" + private const val PAYMENT_DATA = "payment_data" + private const val HTTP_FRAUD_CODE = 403 + private const val CHALLENGE_CANCELED = "Challenge canceled." + private val TAG = AdyenTopUpPresenter::class.java.name + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/topup/payment/AdyenTopUpView.kt b/app/src/main/java/com/asfoundation/wallet/topup/payment/AdyenTopUpView.kt new file mode 100644 index 00000000000..05dde77e5e3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/payment/AdyenTopUpView.kt @@ -0,0 +1,82 @@ +package com.asfoundation.wallet.topup.payment + +import android.net.Uri +import android.os.Bundle +import androidx.annotation.StringRes +import com.adyen.checkout.base.model.paymentmethods.PaymentMethod +import com.adyen.checkout.base.model.payments.response.Action +import com.asfoundation.wallet.billing.address.BillingAddressModel +import com.asfoundation.wallet.billing.adyen.AdyenCardWrapper +import com.asfoundation.wallet.billing.adyen.AdyenComponentResponseModel +import io.reactivex.Observable +import java.math.BigDecimal + +interface AdyenTopUpView { + + fun showValues(value: String, currency: String) + + fun showLoading() + + fun hideLoading() + + fun showNetworkError() + + fun updateTopUpButton(valid: Boolean) + + fun cancelPayment() + + fun setFinishingPurchase() + + fun finishCardConfiguration(paymentMethod: PaymentMethod, isStored: Boolean, forget: Boolean, + savedInstanceState: Bundle?) + + fun setupRedirectComponent() + + fun forgetCardClick(): Observable + + fun submitUriResult(uri: Uri) + + fun getPaymentDetails(): Observable + + fun showSpecificError(stringRes: Int) + + fun showCvvError() + + fun topUpButtonClicked(): Observable + + fun billingAddressInput(): Observable + + fun retrievePaymentData(): Observable + + fun retrieveBillingAddressData(): BillingAddressModel? + + fun hideKeyboard() + + fun getTryAgainClicks(): Observable + + fun getSupportClicks(): Observable + + fun lockRotation() + + fun retryClick(): Observable + + fun hideErrorViews() + + fun showRetryAnimation() + + fun navigateToPaymentSelection() + + fun setupUi() + + fun showBonus(bonus: BigDecimal, currency: String) + + fun showWalletValidation(@StringRes error: Int) + + fun handle3DSAction(action: Action) + + fun onAdyen3DSError(): Observable + + fun setup3DSComponent() + + fun navigateToBillingAddress(fiatAmount: String, fiatCurrency: String) +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/payment/PaymentFragmentNavigator.kt b/app/src/main/java/com/asfoundation/wallet/topup/payment/PaymentFragmentNavigator.kt new file mode 100644 index 00000000000..289728a5f45 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/payment/PaymentFragmentNavigator.kt @@ -0,0 +1,28 @@ +package com.asfoundation.wallet.topup.payment + +import android.net.Uri +import android.os.Bundle +import com.asfoundation.wallet.navigator.UriNavigator +import com.asfoundation.wallet.topup.TopUpActivityView +import com.asfoundation.wallet.ui.iab.Navigator +import io.reactivex.Observable + +class PaymentFragmentNavigator(private val uriNavigator: UriNavigator, + private val topUpView: TopUpActivityView) : Navigator { + + override fun popView(bundle: Bundle) { + topUpView.finish(bundle) + } + + override fun popViewWithError() { + topUpView.close(false) + } + + override fun navigateToUriForResult(redirectUrl: String) { + uriNavigator.navigateToUri(redirectUrl) + } + + override fun uriResults(): Observable { + return uriNavigator.uriResults() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/topup/paymentMethods/TopUpPaymentMethodsAdapter.kt b/app/src/main/java/com/asfoundation/wallet/topup/paymentMethods/TopUpPaymentMethodsAdapter.kt new file mode 100644 index 00000000000..52db6d62580 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/topup/paymentMethods/TopUpPaymentMethodsAdapter.kt @@ -0,0 +1,39 @@ +package com.asfoundation.wallet.topup.paymentMethods + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R +import com.asfoundation.wallet.ui.iab.PaymentMethod +import com.asfoundation.wallet.ui.iab.PaymentMethodsViewHolder +import com.jakewharton.rxrelay2.PublishRelay + + +class TopUpPaymentMethodsAdapter(private var paymentMethods: List, + private var paymentMethodClick: PublishRelay) : + RecyclerView.Adapter() { + private var selectedItem = 0 + + fun setSelectedItem(position: Int) { + selectedItem = position + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentMethodsViewHolder { + return PaymentMethodsViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_payment_method, parent, false)) + } + + override fun getItemCount() = paymentMethods.size + + override fun onBindViewHolder(holder: PaymentMethodsViewHolder, position: Int) { + holder.bind(paymentMethods[position], selectedItem == position, View.OnClickListener { + selectedItem = position + paymentMethodClick.accept(paymentMethods[position].id) + notifyDataSetChanged() + }) + } + + fun getSelectedItemData(): PaymentMethod = paymentMethods[selectedItem] +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/transactions/Operation.java b/app/src/main/java/com/asfoundation/wallet/transactions/Operation.java index 1cf159942f2..3523eb01573 100644 --- a/app/src/main/java/com/asfoundation/wallet/transactions/Operation.java +++ b/app/src/main/java/com/asfoundation/wallet/transactions/Operation.java @@ -20,7 +20,7 @@ public class Operation implements Parcelable { private String to; private String fee; - Operation(String transactionId, String from, String to, String fee) { + public Operation(String transactionId, String from, String to, String fee) { this.transactionId = transactionId; this.from = from; this.to = to; diff --git a/app/src/main/java/com/asfoundation/wallet/transactions/PerkBonusService.kt b/app/src/main/java/com/asfoundation/wallet/transactions/PerkBonusService.kt new file mode 100644 index 00000000000..aabbd59e507 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/transactions/PerkBonusService.kt @@ -0,0 +1,126 @@ +package com.asfoundation.wallet.transactions + +import android.app.IntentService +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import com.asf.wallet.R +import com.asfoundation.wallet.C +import com.asfoundation.wallet.repository.TransactionRepositoryType +import com.asfoundation.wallet.ui.TransactionsActivity +import com.asfoundation.wallet.util.CurrencyFormatUtils +import dagger.android.AndroidInjection +import java.math.BigDecimal +import java.math.RoundingMode +import javax.inject.Inject +import kotlin.math.pow + +class PerkBonusService : IntentService(PerkBonusService::class.java.simpleName) { + + @Inject + lateinit var transactionRepository: TransactionRepositoryType + + @Inject + lateinit var formatter: CurrencyFormatUtils + + private lateinit var notificationManager: NotificationManager + + override fun onCreate() { + super.onCreate() + AndroidInjection.inject(this) + } + + override fun onHandleIntent(intent: Intent?) { + val address = intent?.getStringExtra(ADDRESS_KEY) + address?.let { + notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + handlePerkTransactionNotification(it) + } + } + + private fun handlePerkTransactionNotification(address: String, timesCalled: Int = 0) { + try { + val transactions = transactionRepository.fetchNewTransactions(address) + .blockingGet() + if (transactions.isEmpty() && timesCalled < 4) { + handlePerkTransactionNotification(address, timesCalled + 1) + } else { + val transactionValue = getPerkBonusTransactionValue(transactions) + if (transactionValue.isNotEmpty()) { + buildNotification(transactionValue) + } + } + } catch (exception: Exception) { + exception.printStackTrace() + } + } + + private fun buildNotification(value: String) { + val notificationBuilder = createNotification(value).build() + notificationManager.notify(NOTIFICATION_SERVICE_ID, notificationBuilder) + } + + private fun createPositiveNotificationIntent(): PendingIntent { + val intent = TransactionsActivity.newIntent(this) + return PendingIntent.getActivity(this, 0, intent, 0) + } + + private fun getPerkBonusTransactionValue(transactions: List): String { + //Empty validation is done in done before on the filter + val lastTransactionTime = transactions[0].processedTime + //To avoid older transactions that may have not yet been inserted in DB we give a small gap + val transactionGap = lastTransactionTime - 15000 + val transaction = + transactions.takeWhile { it.processedTime >= transactionGap } + .find { it.subType == Transaction.SubType.PERK_PROMOTION } + return getScaledValue(transaction?.value) ?: "" + } + + private fun createNotification(value: String): NotificationCompat.Builder { + val positiveIntent = createPositiveNotificationIntent() + val builder: NotificationCompat.Builder + val channelId = "notification_channel_perk_bonus" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channelName: CharSequence = "Notification channel" + val importance = NotificationManager.IMPORTANCE_HIGH + val notificationChannel = + NotificationChannel(channelId, channelName, importance) + builder = NotificationCompat.Builder(this, channelId) + notificationManager.createNotificationChannel(notificationChannel) + } else { + builder = NotificationCompat.Builder(this, channelId) + builder.setVibrate(LongArray(0)) + } + return builder.setContentTitle(getString(R.string.perks_notification, value)) + .setAutoCancel(true) + .setContentIntent(positiveIntent) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentText(getString(R.string.support_new_message_button)) + } + + private fun getScaledValue(valueStr: String?): String? { + if (valueStr == null) return null + var value = BigDecimal(valueStr) + value = value.divide(BigDecimal(10.0.pow(C.ETHER_DECIMALS.toDouble())), 2, RoundingMode.FLOOR) + if (value <= BigDecimal.ZERO) return null + return formatter.formatGamificationValues(value) + } + + companion object { + private const val NOTIFICATION_SERVICE_ID = 77796 + private const val ADDRESS_KEY = "ADDRESS_KEY" + + @JvmStatic + fun buildService(context: Context, address: String) { + Intent(context, PerkBonusService::class.java).also { intent -> + intent.putExtra(ADDRESS_KEY, address) + context.startService(intent) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/transactions/Transaction.java b/app/src/main/java/com/asfoundation/wallet/transactions/Transaction.java index 11f4247ec9f..fe707b0a09c 100644 --- a/app/src/main/java/com/asfoundation/wallet/transactions/Transaction.java +++ b/app/src/main/java/com/asfoundation/wallet/transactions/Transaction.java @@ -2,7 +2,6 @@ import android.os.Parcel; import android.os.Parcelable; -import com.asfoundation.wallet.entity.TransactionContract; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -10,25 +9,47 @@ import javax.annotation.Nullable; public class Transaction implements Parcelable { + public static final Creator CREATOR = new Creator() { + @Override public Transaction createFromParcel(Parcel in) { + return new Transaction(in); + } + + @Override public Transaction[] newArray(int size) { + return new Transaction[size]; + } + }; private final String transactionId; - @Nullable private final String approveTransactionId; + @Nullable private final SubType subType; + @Nullable private final String title; + @Nullable private final String description; + @Nullable private final Perk perk; + private final String approveTransactionId; private final TransactionType type; private final long timeStamp; + private final long processedTime; private final TransactionStatus status; private final String value; private final String from; private final String to; - private final TransactionDetails details; - private final String currency; - private final List operations; + @Nullable private final TransactionDetails details; + @Nullable private final String currency; + @Nullable private final List operations; - public Transaction(String transactionId, TransactionType type, - @Nullable String approveTransactionId, long timeStamp, TransactionStatus status, String value, - String from, String to, @Nullable TransactionDetails details, String currency, List operations) { + public Transaction(String transactionId, TransactionType type, @Nullable SubType subType, + @Nullable String title, @Nullable String description, @Nullable Perk perk, + @Nullable String approveTransactionId, long timeStamp, long processedTime, + TransactionStatus status, String value, String from, String to, + @Nullable TransactionDetails details, @Nullable String currency, + @Nullable List operations) { this.transactionId = transactionId; + this.subType = subType; + this.title = title; + this.description = description; + this.perk = perk; this.approveTransactionId = approveTransactionId; this.type = type; this.timeStamp = timeStamp; + this.processedTime = processedTime; this.status = status; this.value = value; this.from = from; @@ -38,15 +59,30 @@ public Transaction(String transactionId, TransactionType type, this.operations = operations; } - public static final Creator CREATOR = new Creator() { - @Override public Transaction createFromParcel(Parcel in) { - return new Transaction(in); - } - - @Override public Transaction[] newArray(int size) { - return new Transaction[size]; + protected Transaction(Parcel in) { + transactionId = in.readString(); + subType = SubType.fromInt(in.readInt()); + title = in.readString(); + description = in.readString(); + perk = Perk.fromInt(in.readInt()); + approveTransactionId = in.readString(); + type = TransactionType.fromInt(in.readInt()); + timeStamp = in.readLong(); + processedTime = in.readLong(); + status = TransactionStatus.fromInt(in.readInt()); + value = in.readString(); + from = in.readString(); + to = in.readString(); + details = in.readParcelable(TransactionDetails.class.getClassLoader()); + currency = in.readString(); + Parcelable[] parcelableArray = in.readParcelableArray(Operation.class.getClassLoader()); + operations = new ArrayList<>(); + if (parcelableArray != null) { + Operation[] operationsArray = + Arrays.copyOf(parcelableArray, parcelableArray.length, Operation[].class); + operations.addAll(Arrays.asList(operationsArray)); } - }; + } @Override public int describeContents() { return 0; @@ -54,46 +90,143 @@ public Transaction(String transactionId, TransactionType type, @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(transactionId); + if (subType != null) { + dest.writeInt(subType.ordinal()); + } else { + dest.writeInt(-1); + } + dest.writeString(title); + dest.writeString(description); + if (perk != null) { + dest.writeInt(perk.ordinal()); + } else { + dest.writeInt(-1); + } dest.writeString(approveTransactionId); dest.writeInt(type.ordinal()); dest.writeLong(timeStamp); + dest.writeLong(processedTime); dest.writeInt(status.ordinal()); dest.writeString(value); dest.writeString(from); dest.writeString(to); dest.writeParcelable(details, flags); dest.writeString(currency); - Operation[] operationsArray = new Operation[operations.size()]; - operations.toArray(operationsArray); + Operation[] operationsArray = new Operation[0]; + if (operations != null) { + operationsArray = new Operation[operations.size()]; + operations.toArray(operationsArray); + } dest.writeParcelableArray(operationsArray, flags); } - protected Transaction(Parcel in) { - transactionId = in.readString(); - approveTransactionId = in.readString(); - type = TransactionType.fromInt(in.readInt()); - timeStamp = in.readLong(); - status = TransactionStatus.fromInt(in.readInt()); - value = in.readString(); - from = in.readString(); - to = in.readString(); - details = in.readParcelable(TransactionDetails.class.getClassLoader());; - currency = in.readString(); - Parcelable[] parcelableArray = - in.readParcelableArray(Operation.class.getClassLoader()); - operations = new ArrayList<>(); - if (parcelableArray != null) { - Operation[] operationsArray = - Arrays.copyOf(parcelableArray, parcelableArray.length, Operation[].class); - operations.addAll(Arrays.asList(operationsArray)); - } + @Override public int hashCode() { + int result = transactionId.hashCode(); + result = 31 * result + (subType != null ? subType.hashCode() : 0); + result = 31 * result + (title != null ? title.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (perk != null ? perk.hashCode() : 0); + result = 31 * result + (approveTransactionId != null ? approveTransactionId.hashCode() : 0); + result = 31 * result + type.hashCode(); + result = 31 * result + (int) (timeStamp ^ (timeStamp >>> 32)); + result = 31 * result + status.hashCode(); + result = 31 * result + value.hashCode(); + result = 31 * result + from.hashCode(); + result = 31 * result + to.hashCode(); + result = 31 * result + (details != null ? details.hashCode() : 0); + result = 31 * result + (currency != null ? currency.hashCode() : 0); + result = 31 * result + (operations != null ? operations.hashCode() : 0); + return result; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Transaction)) return false; + + Transaction that = (Transaction) o; + if (timeStamp != that.timeStamp) return false; + if (!transactionId.equals(that.transactionId)) return false; + if (!Objects.equals(subType, that.subType)) return false; + if (!Objects.equals(title, that.title)) return false; + if (!Objects.equals(description, that.description)) return false; + if (!Objects.equals(perk, that.perk)) return false; + if (!Objects.equals(approveTransactionId, that.approveTransactionId)) return false; + if (type != that.type) return false; + if (status != that.status) return false; + if (!value.equals(that.value)) return false; + if (!from.equals(that.from)) return false; + if (!to.equals(that.to)) return false; + if (!Objects.equals(details, that.details)) return false; + if (!Objects.equals(currency, that.currency)) return false; + return Objects.equals(operations, that.operations); } - public String getApproveTransactionId() { + @Override public String toString() { + return "Transaction{" + + "transactionId='" + + transactionId + + '\'' + + ", approveTransactionId='" + + approveTransactionId + + '\'' + + ", type=" + + type + + '\'' + + ", subType=" + + subType + + '\'' + + ", title=" + + title + + '\'' + + ", description=" + + description + + '\'' + + ", perk=" + + perk + + ", timeStamp=" + + timeStamp + + ", status=" + + status + + ", value='" + + value + + '\'' + + ", from='" + + from + + '\'' + + ", to='" + + to + + '\'' + + ", details=" + + details + + ", currency='" + + currency + + '\'' + + ", operations=" + + operations + + '}'; + } + + @Nullable public String getApproveTransactionId() { return approveTransactionId; } + @Nullable public SubType getSubType() { + return subType; + } + + @Nullable public Perk getPerk() { + return perk; + } + + @Nullable public String getTitle() { + return title; + } + + @Nullable public String getDescription() { + return description; + } + public String getTransactionId() { return transactionId; } @@ -102,6 +235,10 @@ public long getTimeStamp() { return timeStamp; } + public long getProcessedTime() { + return processedTime; + } + public TransactionType getType() { return type; } @@ -122,32 +259,75 @@ public String getTo() { return to; } - public TransactionDetails getDetails() { + @Nullable public TransactionDetails getDetails() { return details; } - public List getOperations() { + @Nullable public List getOperations() { return operations; } - public String getCurrency() { + @Nullable public String getCurrency() { return currency; } public enum TransactionType { - STANDARD, IAB, ADS; + STANDARD, IAP, ADS, IAP_OFFCHAIN, ADS_OFFCHAIN, BONUS, TOP_UP, TRANSFER_OFF_CHAIN, + ETHER_TRANSFER; static TransactionType fromInt(int type) { - switch (type) { - case 0: - return STANDARD; - case 1: - return IAB; - case 2: - return ADS; - default: - return STANDARD; - } + switch (type) { + case 1: + return IAP; + case 2: + return ADS; + case 3: + return IAP_OFFCHAIN; + case 4: + return ADS_OFFCHAIN; + case 5: + return BONUS; + case 6: + return TOP_UP; + case 7: + return TRANSFER_OFF_CHAIN; + default: + return STANDARD; + } + } + } + + public enum SubType { + PERK_PROMOTION, UNKNOWN; + + static SubType fromInt(int type) { + if (type != -1) { + if (type == 0) { + return PERK_PROMOTION; + } else { + return UNKNOWN; + } + } else { + return null; + } + } + } + + public enum Perk { + GAMIFICATION_LEVEL_UP, PACKAGE_PERK, UNKNOWN; + + static Perk fromInt(int type) { + if (type != -1) { + if (type == 0) { + return GAMIFICATION_LEVEL_UP; + } else if (type == 1) { + return PACKAGE_PERK; + } else { + return UNKNOWN; + } + } else { + return null; + } } } @@ -156,8 +336,6 @@ public enum TransactionStatus { static TransactionStatus fromInt(int status) { switch (status) { - case 0: - return SUCCESS; case 1: return FAILED; case 2: @@ -167,60 +345,4 @@ static TransactionStatus fromInt(int status) { } } } - - @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Transaction that = (Transaction) o; - return timeStamp == that.timeStamp - && Objects.equals(transactionId, that.transactionId) - && Objects.equals(approveTransactionId, that.approveTransactionId) - && type == that.type - && status == that.status - && Objects.equals(value, that.value) - && Objects.equals(from, that.from) - && Objects.equals(to, that.to) - && Objects.equals(details, that.details) - && Objects.equals(currency, that.currency) - && Objects.equals(operations, that.operations); - } - - @Override public int hashCode() { - return Objects.hash(transactionId, approveTransactionId, type, timeStamp, status, value, from, - to, details, currency, operations); - } - - @Override public String toString() { - return "Transaction{" - + "transactionId='" - + transactionId - + '\'' - + ", approveTransactionId='" - + approveTransactionId - + '\'' - + ", type=" - + type - + ", timeStamp=" - + timeStamp - + ", status=" - + status - + ", value='" - + value - + '\'' - + ", from='" - + from - + '\'' - + ", to='" - + to - + '\'' - + ", details='" - + details - + '\'' - + ", currency='" - + currency - + '\'' - + ", operations=" - + operations - + '}'; - } } diff --git a/app/src/main/java/com/asfoundation/wallet/transactions/TransactionDetails.java b/app/src/main/java/com/asfoundation/wallet/transactions/TransactionDetails.java index 4d9401397a2..b8593d5f8a0 100644 --- a/app/src/main/java/com/asfoundation/wallet/transactions/TransactionDetails.java +++ b/app/src/main/java/com/asfoundation/wallet/transactions/TransactionDetails.java @@ -3,37 +3,12 @@ import android.os.Parcel; import android.os.Parcelable; import java.util.Objects; +import javax.annotation.Nullable; /** * Created by Joao Raimundo on 18/05/2018. */ -public class TransactionDetails implements Parcelable { - - String sourceName; - String icon; - String description; - - public TransactionDetails(String sourceName, String icon, String description) { - this.sourceName = sourceName; - this.icon = icon; - this.description = description; - } - - protected TransactionDetails(Parcel in) { - sourceName = in.readString(); - icon = in.readString(); - description = in.readString(); - } - - @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(sourceName); - dest.writeString(icon); - dest.writeString(description); - } - - @Override public int describeContents() { - return 0; - } +public class TransactionDetails implements Parcelable { public static final Creator CREATOR = new Creator() { @Override public TransactionDetails createFromParcel(Parcel in) { @@ -44,30 +19,42 @@ protected TransactionDetails(Parcel in) { return new TransactionDetails[size]; } }; + String sourceName; + Icon icon; + String description; - public String getSourceName() { - return sourceName; + public TransactionDetails(@Nullable String sourceName, Icon icon, String description) { + this.sourceName = sourceName; + this.icon = icon; + this.description = description; } - public String getIcon() { - return icon; + protected TransactionDetails(Parcel in) { + sourceName = in.readString(); + String iconSource = in.readString(); + String iconType = in.readString(); + icon = new Icon(Icon.Type.valueOf(iconType), iconSource); + description = in.readString(); } - public String getDescription() { - return description; + @Override public int hashCode() { + int result = sourceName != null ? sourceName.hashCode() : 0; + result = 31 * result + (icon != null ? icon.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + return result; } @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof TransactionDetails)) return false; + TransactionDetails that = (TransactionDetails) o; - return Objects.equals(sourceName, that.sourceName) - && Objects.equals(icon, that.icon) - && Objects.equals(description, that.description); - } - @Override public int hashCode() { - return Objects.hash(sourceName, icon, description); + if (!Objects.equals(sourceName, that.sourceName)) { + return false; + } + if (!Objects.equals(icon, that.icon)) return false; + return Objects.equals(description, that.description); } @Override public String toString() { @@ -83,4 +70,65 @@ public String getDescription() { + '\'' + '}'; } + + @Override public int describeContents() { + return 0; + } + + @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(sourceName); + dest.writeString(icon.uri); + dest.writeString(icon.type.name()); + dest.writeString(description); + } + + public Icon getIcon() { + return icon; + } + + public String getSourceName() { + return sourceName; + } + + public String getDescription() { + return description; + } + + public static class Icon { + private final Type type; + private final String uri; + + public Icon(Type type, String uri) { + this.type = type; + this.uri = uri; + } + + public Type getType() { + return type; + } + + public String getUri() { + return uri; + } + + @Override public int hashCode() { + int result = type.hashCode(); + result = 31 * result + uri.hashCode(); + return result; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Icon)) return false; + + Icon icon = (Icon) o; + + if (type != icon.type) return false; + return uri.equals(icon.uri); + } + + public enum Type { + FILE, URL + } + } } diff --git a/app/src/main/java/com/asfoundation/wallet/transactions/TransactionsAnalytics.kt b/app/src/main/java/com/asfoundation/wallet/transactions/TransactionsAnalytics.kt new file mode 100644 index 00000000000..d9b1e7924d7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/transactions/TransactionsAnalytics.kt @@ -0,0 +1,18 @@ +package com.asfoundation.wallet.transactions + +import cm.aptoide.analytics.AnalyticsManager + +class TransactionsAnalytics(private val analytics: AnalyticsManager) { + companion object { + const val OPEN_APPLICATION = "OPEN_APPLICATION" + private const val UNIQUE_NAME = "unique_name" + private const val PACKAGE_NAME = "package_name" + private const val WALLET = "WALLET" + } + + fun openApp(uniqueName: String, packageName: String) { + analytics.logEvent( + hashMapOf(Pair(UNIQUE_NAME, uniqueName), Pair(PACKAGE_NAME, packageName)), + OPEN_APPLICATION, AnalyticsManager.Action.OPEN, WALLET) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/transactions/TransactionsMapper.java b/app/src/main/java/com/asfoundation/wallet/transactions/TransactionsMapper.java deleted file mode 100644 index ac7f1a58f93..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/transactions/TransactionsMapper.java +++ /dev/null @@ -1,225 +0,0 @@ -package com.asfoundation.wallet.transactions; - -import android.support.annotation.Nullable; -import com.asfoundation.wallet.entity.RawTransaction; -import com.asfoundation.wallet.entity.TransactionOperation; -import com.asfoundation.wallet.interact.DefaultTokenProvider; -import com.asfoundation.wallet.ui.iab.AppcoinsOperationsDataSaver; -import com.asfoundation.wallet.ui.iab.AppCoinsOperation; -import com.asfoundation.wallet.util.BalanceUtils; -import io.reactivex.Scheduler; -import io.reactivex.Single; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; - -public class TransactionsMapper { - public static final String APPROVE_METHOD_ID = "0x095ea7b3"; - public static final String BUY_METHOD_ID = "0xdc9564d5"; - public static final String ADS_METHOD_ID = "0x79c6b667"; - private final DefaultTokenProvider defaultTokenProvider; - private final AppcoinsOperationsDataSaver operationsDataSaver; - private final Scheduler scheduler; - - public TransactionsMapper(DefaultTokenProvider defaultTokenProvider, - AppcoinsOperationsDataSaver operationsDataSaver, Scheduler scheduler) { - this.defaultTokenProvider = defaultTokenProvider; - this.operationsDataSaver = operationsDataSaver; - this.scheduler = scheduler; - } - - public Single> map(RawTransaction[] transactions) { - return defaultTokenProvider.getDefaultToken().observeOn(scheduler) - .map(tokenInfo -> map(tokenInfo.address, transactions)); - } - - private List map(String address, RawTransaction[] transactions) { - List transactionList = new ArrayList<>(); - for (int i = transactions.length - 1; i >= 0; i--) { - RawTransaction transaction = transactions[i]; - if (isAppcoinsTransaction(transaction, address) && isApprovedTransaction(transaction) && (i - > 0 && isIabTransaction(transactions[i - 1]))) { - transactionList.add(0, mapIabTransaction(transaction, transactions[i - 1])); - i--; - } else if (isAdsTransaction(transaction)) { - transactionList.add(0, mapAdsTransaction(transaction)); - } else { - transactionList.add(0, mapStandardTransaction(transaction)); - } - } - return transactionList; - } - - /** - * Method to map a raw transaction to an Ads transaction. In this case the raw transaction value - * does not contain the value of the transfer, that information is in the operations contained in - * the raw transaction. - * NOTE: For the value of this transaction we are considering the value of the first operation, - * by relying on the order that the ads transactions are done. Only the first operation includes - * the value that will be earned by the user. - * - * @param transaction The raw transaction including all the information for a given transaction. - * - * @return a Transaction object containing the information needed and formatted, ready to be shown - * on the transactions list. - */ - private Transaction mapAdsTransaction(RawTransaction transaction) { - String value = transaction.value; - String currency = null; - String from = transaction.from; - String to = transaction.to; - List operations = new ArrayList<>(); - String fee = BalanceUtils.weiToEth( - new BigDecimal(transaction.gasUsed).multiply(new BigDecimal(transaction.gasPrice))) - .toPlainString(); - - if (transaction.operations != null && transaction.operations.length > 0) { - TransactionOperation operation = transaction.operations[0]; - value = operation.value; - currency = operation.contract.symbol; - from = operation.from; - to = operation.to; - - operations.add(new Operation(transaction.hash, operation.from, operation.to, fee)); - } else { - - operations.add( - new Operation(transaction.hash, transaction.from, transaction.to, fee)); - } - - TransactionDetails details = getTransactionDetails(Transaction.TransactionType.ADS, transaction); - - return new Transaction(transaction.hash, Transaction.TransactionType.ADS, null, - transaction.timeStamp, getError(transaction), value, from, to, details, currency, - operations); - } - - private boolean isAdsTransaction(RawTransaction transaction) { - return transaction.input.toUpperCase() - .startsWith(ADS_METHOD_ID.toUpperCase()); - } - - /** - * Method to map a raw transaction to a standard transaction. In this case most probably the raw - * transaction value contains the value of the transfer, but to make sure that is the case, we - * confirm that there is no operation inside the raw transaction. In case the operations list is - * not empty we make the assumption that the value on the first operation of the list is the one - * to be taken in consideration for the user. - * - * @param transaction The raw transaction including all the information for a given transaction. - * - * @return a Transaction object containing the information needed and formatted, ready to be shown - * on the transactions list. - */ - private Transaction mapStandardTransaction(RawTransaction transaction) { - String value = transaction.value; - String currency = null; - List operations = new ArrayList<>(); - String fee = BalanceUtils.weiToEth( - new BigDecimal(transaction.gasUsed).multiply(new BigDecimal(transaction.gasPrice))) - .toPlainString(); - - if (transaction.operations != null && transaction.operations.length > 0) { - TransactionOperation operation = transaction.operations[0]; - value = operation.value; - currency = operation.contract.symbol; - - operations.add(new Operation(transaction.hash, operation.from, operation.to, fee)); - } else { - - operations.add( - new Operation(transaction.hash, transaction.from, transaction.to, fee)); - } - - return new Transaction(transaction.hash, Transaction.TransactionType.STANDARD, null, - transaction.timeStamp, getError(transaction), value, transaction.from, transaction.to, null, - currency, operations); - } - - /** - * Method to map a raw transaction to an IAB transaction. In this case all the transfer mentioned - * in the operations list on the raw transaction need to be summed to obtained the value of the - * transaction, since the user transfer the value that afterwards is split between all the parties - * included in the iab transaction. - * - * @param approveTransaction The raw transaction for the approve transaction. - * @param transaction The raw transaction including all the information for a given transaction. - * - * @return a Transaction object containing the information needed and formatted, ready to be shown - * on the transactions list. - */ - private Transaction mapIabTransaction(RawTransaction approveTransaction, - RawTransaction transaction) { - BigInteger value = new BigInteger(transaction.value); - String currency = null; - List operations = new ArrayList<>(); - - String fee = BalanceUtils.weiToEth(new BigDecimal(approveTransaction.gasUsed).multiply( - new BigDecimal(approveTransaction.gasPrice))) - .toPlainString(); - if (approveTransaction.operations != null && approveTransaction.operations.length > 0) { - currency = approveTransaction.operations[0].contract.symbol; - - operations.add( - new Operation(approveTransaction.hash, approveTransaction.from, approveTransaction.to, - fee)); - } else { - operations.add( - new Operation(approveTransaction.hash, approveTransaction.from, approveTransaction.to, - fee)); - } - - fee = BalanceUtils.weiToEth( - new BigDecimal(transaction.gasUsed).multiply(new BigDecimal(transaction.gasPrice))) - .toPlainString(); - if (transaction.operations != null && transaction.operations.length > 0) { - currency = transaction.operations[0].contract.symbol; - for (TransactionOperation operation : transaction.operations) { - value = value.add(new BigInteger(operation.value)); - } - - operations.add( - new Operation(transaction.hash, transaction.from, transaction.to, fee)); - } - - TransactionDetails details = getTransactionDetails(Transaction.TransactionType.IAB, transaction); - - return new Transaction(transaction.hash, Transaction.TransactionType.IAB, - approveTransaction.hash, transaction.timeStamp, getError(transaction), value.toString(), - transaction.from, transaction.to, details, currency, operations); - } - - private boolean isIabTransaction(RawTransaction auxTransaction) { - return auxTransaction.input.toUpperCase() - .startsWith(BUY_METHOD_ID.toUpperCase()); - } - - private boolean isApprovedTransaction(RawTransaction transaction) { - return transaction.input.toUpperCase() - .startsWith(APPROVE_METHOD_ID.toUpperCase()); - } - - private boolean isAppcoinsTransaction(RawTransaction transaction, String address) { - return transaction.to.equalsIgnoreCase(address); - } - - private Transaction.TransactionStatus getError(RawTransaction transaction) { - return (transaction.error == null || transaction.error.isEmpty()) - ? Transaction.TransactionStatus.SUCCESS : Transaction.TransactionStatus.FAILED; - } - - @Nullable private TransactionDetails getTransactionDetails(Transaction.TransactionType type, RawTransaction transaction) { - TransactionDetails details = null; - AppCoinsOperation operationDetails = operationsDataSaver.getSync(transaction.hash); - if (operationDetails != null) { - String productName = null; - if (!Transaction.TransactionType.ADS.equals(type)) { - productName = operationDetails.getProductName(); - } - details = new TransactionDetails(operationDetails.getApplicationName(), - operationDetails.getIconPath(), productName); - } - return details; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/transactions/TransactionsMapper.kt b/app/src/main/java/com/asfoundation/wallet/transactions/TransactionsMapper.kt new file mode 100644 index 00000000000..84e2e66c8f8 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/transactions/TransactionsMapper.kt @@ -0,0 +1,101 @@ +package com.asfoundation.wallet.transactions + +import com.asfoundation.wallet.entity.WalletHistory +import com.asfoundation.wallet.transactions.TransactionDetails +import java.util.* + +class TransactionsMapper { + + fun mapTransactionsFromWalletHistory( + transactions: List): List { + + val transactionList: MutableList = ArrayList(transactions.size) + + for (i in transactions.indices.reversed()) { + val transaction = transactions[i] + val txType = mapTransactionType(transaction) + val status: Transaction.TransactionStatus = when (transaction.status) { + WalletHistory.Status.SUCCESS -> Transaction.TransactionStatus.SUCCESS + WalletHistory.Status.FAIL -> Transaction.TransactionStatus.FAILED + else -> Transaction.TransactionStatus.FAILED + } + val sourceName = if (txType == Transaction.TransactionType.BONUS) { + if (transaction.bonus == null) { + null + } else { + transaction.bonus.stripTrailingZeros() + .toPlainString() + } + } else { + transaction.app + } + val bonusSubType = mapSubtype(transaction.subType) + val perk = mapPerk(transaction.perk) + transactionList.add(0, + Transaction(transaction.txID, txType, bonusSubType, transaction.title, + transaction.description, perk, null, transaction.ts.time, + transaction.processedTime.time, status, transaction.amount.toString(), + transaction.sender, transaction.receiver, TransactionDetails(sourceName, + TransactionDetails.Icon(TransactionDetails.Icon.Type.URL, transaction.icon), + transaction.sku), + if (txType == Transaction.TransactionType.ETHER_TRANSFER) "ETH" else "APPC", + mapOperations(transaction.operations))) + } + return transactionList + } + + private fun mapPerk(perk: String?): Transaction.Perk? { + var perkType: Transaction.Perk? = null + if (perk != null) { + perkType = when (perk) { + GAMIFICATION_LEVEL_UP -> Transaction.Perk.GAMIFICATION_LEVEL_UP + PACKAGE_PERK -> Transaction.Perk.PACKAGE_PERK + else -> Transaction.Perk.UNKNOWN + } + } + return perkType + } + + private fun mapSubtype(subType: String?): Transaction.SubType? { + var bonusSubType: Transaction.SubType? = null + if (subType != null) { + bonusSubType = if (subType == PERK_BONUS) { + Transaction.SubType.PERK_PROMOTION + } else { + Transaction.SubType.UNKNOWN + } + } + return bonusSubType + } + + private fun mapOperations(operations: List): List { + val list: MutableList = + ArrayList(operations.size) + for (operation in operations) { + list.add(Operation(operation.transactionId, + operation.sender, + operation.receiver, operation.fee)) + } + return list + } + + private fun mapTransactionType( + transaction: WalletHistory.Transaction): Transaction.TransactionType { + return when (transaction.type) { + "Transfer OffChain" -> Transaction.TransactionType.TRANSFER_OFF_CHAIN + "Topup OffChain" -> Transaction.TransactionType.TOP_UP + "IAP OffChain" -> Transaction.TransactionType.IAP_OFFCHAIN + "bonus" -> Transaction.TransactionType.BONUS + "PoA OffChain" -> Transaction.TransactionType.ADS_OFFCHAIN + "Ether Transfer" -> Transaction.TransactionType.ETHER_TRANSFER + "IAP" -> Transaction.TransactionType.IAP + else -> Transaction.TransactionType.STANDARD + } + } + + companion object { + private const val PERK_BONUS = "perk_bonus" + private const val GAMIFICATION_LEVEL_UP = "GAMIFICATION_LEVEL_UP" + private const val PACKAGE_PERK = "PACKAGE_PERK" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/ActivityResultSharer.kt b/app/src/main/java/com/asfoundation/wallet/ui/ActivityResultSharer.kt new file mode 100644 index 00000000000..613e9da14b8 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/ActivityResultSharer.kt @@ -0,0 +1,12 @@ +package com.asfoundation.wallet.ui + +import android.content.Intent + +interface ActivityResultSharer { + fun addOnActivityListener(listener: ActivityResultListener) + fun remove(listener: ActivityResultListener) + + interface ActivityResultListener { + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/AddTokenActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/AddTokenActivity.java deleted file mode 100644 index c81d85548bf..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/AddTokenActivity.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.asfoundation.wallet.ui; - -import android.app.Dialog; -import android.arch.lifecycle.ViewModelProviders; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.TextInputLayout; -import android.support.v7.app.AlertDialog; -import android.text.TextUtils; -import android.view.View; -import android.widget.TextView; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.Address; -import com.asfoundation.wallet.entity.ErrorEnvelope; -import com.asfoundation.wallet.viewmodel.AddTokenViewModel; -import com.asfoundation.wallet.viewmodel.AddTokenViewModelFactory; -import com.asfoundation.wallet.widget.SystemView; -import dagger.android.AndroidInjection; -import javax.inject.Inject; - -public class AddTokenActivity extends BaseActivity implements View.OnClickListener { - - @Inject protected AddTokenViewModelFactory addTokenViewModelFactory; - private AddTokenViewModel viewModel; - - private TextInputLayout addressLayout; - private TextView address; - private TextInputLayout symbolLayout; - private TextView symbol; - private TextInputLayout decimalsLayout; - private TextView decimals; - private SystemView systemView; - private Dialog dialog; - - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { - AndroidInjection.inject(this); - - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_add_token); - - toolbar(); - - addressLayout = findViewById(R.id.address_input_layout); - address = findViewById(R.id.address); - symbolLayout = findViewById(R.id.symbol_input_layout); - symbol = findViewById(R.id.symbol); - decimalsLayout = findViewById(R.id.decimal_input_layout); - decimals = findViewById(R.id.decimals); - systemView = findViewById(R.id.system_view); - systemView.hide(); - - findViewById(R.id.save).setOnClickListener(this); - - viewModel = ViewModelProviders.of(this, addTokenViewModelFactory) - .get(AddTokenViewModel.class); - viewModel.progress() - .observe(this, systemView::showProgress); - viewModel.error() - .observe(this, this::onError); - viewModel.result() - .observe(this, this::onSaved); - } - - private void onSaved(boolean result) { - if (result) { - viewModel.showTokens(this); - finish(); - } - } - - private void onError(ErrorEnvelope errorEnvelope) { - dialog = new AlertDialog.Builder(this).setTitle(R.string.title_dialog_error) - .setMessage(R.string.error_add_token) - .setPositiveButton(R.string.try_again, null) - .create(); - dialog.show(); - } - - @Override public void onClick(View v) { - switch (v.getId()) { - case R.id.save: { - onSave(); - } - break; - } - } - - private void onSave() { - boolean isValid = true; - String address = this.address.getText() - .toString() - .toLowerCase(); - String symbol = this.symbol.getText() - .toString() - .toLowerCase(); - String rawDecimals = this.decimals.getText() - .toString(); - int decimals = 0; - - if (TextUtils.isEmpty(address)) { - addressLayout.setError(getString(R.string.error_field_required)); - isValid = false; - } - - if (TextUtils.isEmpty(symbol)) { - symbolLayout.setError(getString(R.string.error_field_required)); - isValid = false; - } - - if (TextUtils.isEmpty(rawDecimals)) { - decimalsLayout.setError(getString(R.string.error_field_required)); - isValid = false; - } - - try { - decimals = Integer.valueOf(rawDecimals); - } catch (NumberFormatException ex) { - decimalsLayout.setError(getString(R.string.error_must_numeric)); - isValid = false; - } - - if (!Address.isAddress(address)) { - addressLayout.setError(getString(R.string.error_invalid_address)); - isValid = false; - } - - if (isValid) { - viewModel.save(address, symbol, decimals); - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/AppcoinsApps.java b/app/src/main/java/com/asfoundation/wallet/ui/AppcoinsApps.java new file mode 100644 index 00000000000..5dfe860c997 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/AppcoinsApps.java @@ -0,0 +1,31 @@ +package com.asfoundation.wallet.ui; + +import com.asfoundation.wallet.apps.Application; +import com.asfoundation.wallet.apps.Applications; +import com.asfoundation.wallet.ui.appcoins.applications.AppcoinsApplication; +import io.reactivex.Single; +import java.util.ArrayList; +import java.util.List; + +public class AppcoinsApps { + private final Applications applications; + + public AppcoinsApps(Applications applications) { + this.applications = applications; + } + + public Single> getApps() { + return applications.getApps() + .map(this::map); + } + + private List map(List apps) { + ArrayList appcoinsApplications = new ArrayList<>(); + for (Application app : apps) { + appcoinsApplications.add( + new AppcoinsApplication(app.getName(), app.getRating(), app.getIconUrl(), + app.getFeaturedGraphic(), app.getPackageName(), app.getUniqueName())); + } + return appcoinsApplications; + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorBottomSheetFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorBottomSheetFragment.kt new file mode 100644 index 00000000000..29a916edf1f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorBottomSheetFragment.kt @@ -0,0 +1,82 @@ +package com.asfoundation.wallet.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.asf.wallet.R +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.authentication_error_bottomsheet.* + +class AuthenticationErrorBottomSheetFragment : Fragment(), AuthenticationErrorBottomSheetView { + + private lateinit var presenter: AuthenticationErrorBottomSheetPresenter + + private val errorTimer: Long by lazy { + if (arguments!!.containsKey(ERROR_TIMER_KEY)) { + arguments!!.getLong(ERROR_TIMER_KEY, 0) + } else { + throw IllegalArgumentException("Error message not found") + } + } + + companion object { + private const val ERROR_TIMER_KEY = "error_message" + + fun newInstance(timer: Long): AuthenticationErrorBottomSheetFragment { + val fragment = AuthenticationErrorBottomSheetFragment() + fragment.arguments = Bundle().apply { + putLong(ERROR_TIMER_KEY, timer) + } + return fragment + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = + AuthenticationErrorBottomSheetPresenter(this, AndroidSchedulers.mainThread(), + CompositeDisposable()) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.authentication_error_bottomsheet, container, false) + } + + override fun getButtonClick() = RxView.clicks(retry_authentication) + + override fun retryAuthentication() { + val parent = provideParentFragment() + parent?.retryAuthentication() + } + + override fun setMessage() { + authentication_error_message.text = getString(R.string.fingerprint_failed_body, errorTimer.toString()) + } + + override fun setupUi() { + val parent = provideParentFragment() + parent?.showBottomSheet() + } + + private fun provideParentFragment(): AuthenticationErrorView? { + if (parentFragment !is AuthenticationErrorView) { + return null + } + return parentFragment as AuthenticationErrorView + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present() + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorBottomSheetPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorBottomSheetPresenter.kt new file mode 100644 index 00000000000..672d96f9997 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorBottomSheetPresenter.kt @@ -0,0 +1,24 @@ +package com.asfoundation.wallet.ui + +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +class AuthenticationErrorBottomSheetPresenter(private val view: AuthenticationErrorBottomSheetView, + private val viewScheduler: Scheduler, + private val disposables: CompositeDisposable) { + + fun present() { + view.setMessage() + view.setupUi() + handleButtonClick() + } + + private fun handleButtonClick() { + disposables.add(view.getButtonClick() + .observeOn(viewScheduler) + .doOnNext { view.retryAuthentication() } + .subscribe({}, { it.printStackTrace() })) + } + + fun stop() = disposables.clear() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorBottomSheetView.kt b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorBottomSheetView.kt new file mode 100644 index 00000000000..7e43377be86 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorBottomSheetView.kt @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.ui + +import io.reactivex.Observable + +interface AuthenticationErrorBottomSheetView { + + fun getButtonClick(): Observable + + fun retryAuthentication() + + fun setMessage() + + fun setupUi() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorFragment.kt new file mode 100644 index 00000000000..215987e3e10 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorFragment.kt @@ -0,0 +1,104 @@ +package com.asfoundation.wallet.ui + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.AnimationUtils +import com.asf.wallet.R +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.authentication_error_fragment.* +import kotlinx.android.synthetic.main.fragment_balance.faded_background + +class AuthenticationErrorFragment : BasePageViewFragment(), AuthenticationErrorView { + + private lateinit var presenter: AuthenticationErrorPresenter + private lateinit var activityView: AuthenticationPromptView + private lateinit var authenticationBottomSheet: BottomSheetBehavior + + private val errorTimer: Long by lazy { + if (arguments!!.containsKey(ERROR_TIMER_KEY)) { + arguments!!.getLong(ERROR_TIMER_KEY, 0) + } else { + throw IllegalArgumentException("Error message not found") + } + } + + companion object { + private const val ERROR_TIMER_KEY = "error_message" + + fun newInstance(timer: Long): AuthenticationErrorFragment { + val fragment = AuthenticationErrorFragment() + fragment.arguments = Bundle().apply { + putLong(ERROR_TIMER_KEY, timer) + } + return fragment + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = AuthenticationErrorPresenter(this, activityView, CompositeDisposable()) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is AuthenticationPromptView) { + throw IllegalStateException( + "AuthenticationError Fragment must be attached to AuthenticationPrompt Activity") + } + activityView = context + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + childFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.fragment_slide_up, R.anim.fragment_slide_down, + R.anim.fragment_slide_up, R.anim.fragment_slide_down) + .replace(R.id.bottom_error_fragment_container, + AuthenticationErrorBottomSheetFragment.newInstance(errorTimer)) + .commit() + return inflater.inflate(R.layout.authentication_error_fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + authenticationBottomSheet = + BottomSheetBehavior.from(bottom_error_fragment_container) + presenter.present() + } + + override fun onDestroyView() { + super.onDestroyView() + faded_background.animation = + AnimationUtils.loadAnimation(context, R.anim.fast_100s_fade_out_animation) + faded_background.visibility = View.GONE + presenter.stop() + } + + override fun showBottomSheet() { + authenticationBottomSheet.state = BottomSheetBehavior.STATE_EXPANDED + authenticationBottomSheet.isFitToContents = true + authenticationBottomSheet.addBottomSheetCallback(object : + BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_DRAGGING) { + authenticationBottomSheet.state = BottomSheetBehavior.STATE_EXPANDED + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit + }) + } + + override fun retryAuthentication() { + activityView.onRetryButtonClick() + } + + override fun outsideOfBottomSheetClick() = RxView.clicks(faded_background) + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorPresenter.kt new file mode 100644 index 00000000000..0bfe0d777b2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorPresenter.kt @@ -0,0 +1,21 @@ +package com.asfoundation.wallet.ui + +import io.reactivex.disposables.CompositeDisposable + +class AuthenticationErrorPresenter( + private val view: AuthenticationErrorView, + private val activityView: AuthenticationPromptView, + private val disposables: CompositeDisposable) { + + fun present() { + handleOutsideOfBottomSheetClick() + } + + private fun handleOutsideOfBottomSheetClick() { + disposables.add(view.outsideOfBottomSheetClick() + .doOnNext { activityView.closeCancel() } + .subscribe({}, { it.printStackTrace() })) + } + + fun stop() = disposables.clear() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorView.kt b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorView.kt new file mode 100644 index 00000000000..c70be97d24e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationErrorView.kt @@ -0,0 +1,12 @@ +package com.asfoundation.wallet.ui + +import io.reactivex.Observable + +interface AuthenticationErrorView { + + fun outsideOfBottomSheetClick(): Observable + + fun showBottomSheet() + + fun retryAuthentication() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationPromptActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationPromptActivity.kt new file mode 100644 index 00000000000..534aacd029b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationPromptActivity.kt @@ -0,0 +1,134 @@ +package com.asfoundation.wallet.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import com.asf.wallet.R +import dagger.android.AndroidInjection +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subjects.PublishSubject +import javax.inject.Inject + +class AuthenticationPromptActivity : BaseActivity(), AuthenticationPromptView { + + @Inject + lateinit var fingerprintInteractor: FingerPrintInteractor + + private lateinit var presenter: AuthenticationPromptPresenter + + private var fingerprintResultSubject: PublishSubject? = null + + private var retryClickSubject: PublishSubject? = null + + companion object { + const val RESULT_OK = 0 + const val RESULT_CANCELED = 1 + + @JvmStatic + fun newIntent(context: Context): Intent { + return Intent(context, AuthenticationPromptActivity::class.java) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.authentication_prompt_activity) + retryClickSubject = PublishSubject.create() + fingerprintResultSubject = PublishSubject.create() + presenter = AuthenticationPromptPresenter(this, AndroidSchedulers.mainThread(), + CompositeDisposable(), fingerprintInteractor) + presenter.present(savedInstanceState) + } + + override fun createBiometricPrompt(): BiometricPrompt { + val executor = ContextCompat.getMainExecutor(this) + return BiometricPrompt(this, executor, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + fingerprintResultSubject?.onNext( + FingerprintAuthResult(errorCode, errString.toString(), null, + FingerprintResult.ERROR)) + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + fingerprintResultSubject?.onNext( + FingerprintAuthResult(null, null, result, FingerprintResult.SUCCESS)) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + fingerprintResultSubject?.onNext( + FingerprintAuthResult(null, null, null, FingerprintResult.FAIL)) + } + }) + } + + override fun onResume() { + super.onResume() + presenter.onResume() + sendPageViewEvent() + } + + override fun getAuthenticationResult(): Observable { + return fingerprintResultSubject!! + } + + override fun getRetryButtonClick(): Observable { + return retryClickSubject!! + } + + override fun onRetryButtonClick() { + retryClickSubject?.onNext("") + } + + override fun showAuthenticationBottomSheet(timer: Long) { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.fade_in_animation, R.anim.fragment_slide_down, + R.anim.fade_in_animation, R.anim.fragment_slide_down) + .replace(R.id.bottom_sheet_error_fragment_container, + AuthenticationErrorFragment.newInstance(timer)) + .commit() + } + + override fun showPrompt(biometricPrompt: BiometricPrompt, deviceCredentialsAllowed: Boolean) { + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.fingerprint_authentication_required_title)) + .setSubtitle(getString(R.string.fingerprint_authentication_required_body)) + .setDeviceCredentialAllowed(deviceCredentialsAllowed) + .build() + biometricPrompt.authenticate(promptInfo) + } + + override fun closeSuccess() { + val intent = Intent() + setResult(RESULT_OK, intent) + finishAndRemoveTask() + } + + override fun closeCancel() { + val intent = Intent() + setResult(RESULT_CANCELED, intent) + finishAndRemoveTask() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + presenter.onSaveInstanceState(outState) + } + + override fun onBackPressed() = closeCancel() + + override fun onDestroy() { + fingerprintResultSubject = null + retryClickSubject = null + presenter.stop() + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationPromptPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationPromptPresenter.kt new file mode 100644 index 00000000000..6bca9243cd0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationPromptPresenter.kt @@ -0,0 +1,103 @@ +package com.asfoundation.wallet.ui + +import android.hardware.biometrics.BiometricManager +import android.os.Bundle +import androidx.biometric.BiometricPrompt +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import kotlin.math.ceil + +class AuthenticationPromptPresenter(private val view: AuthenticationPromptView, + private val viewScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val fingerprintInteractor: FingerPrintInteractor) { + + private var hasBottomsheetOn = false + + companion object { + private const val BOTTOMSHEET_KEY = "bottomsheet_key" + private const val ERROR_RETRY_TIME_IN_MILLIS = 30000 + } + + fun present(savedInstanceState: Bundle?) { + savedInstanceState?.let { + hasBottomsheetOn = it.getBoolean(BOTTOMSHEET_KEY) + } + handleAuthenticationResult() + handleRetryAuthentication() + } + + private fun showBiometricPrompt() { + when (fingerprintInteractor.getDeviceCompatibility()) { + BiometricManager.BIOMETRIC_SUCCESS -> view.showPrompt(view.createBiometricPrompt(), true) + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> view.closeSuccess() + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> view.closeSuccess() + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { + fingerprintInteractor.setAuthenticationPermission(false) + view.closeSuccess() + } + } + } + + private fun showBottomSheet(timer: Long) { + hasBottomsheetOn = true + view.showAuthenticationBottomSheet(timer) + } + + private fun handleAuthenticationResult() { + disposables.add(view.getAuthenticationResult() + .observeOn(viewScheduler) + .doOnNext { + when (it.type) { + FingerprintResult.SUCCESS -> view.closeSuccess() + FingerprintResult.ERROR -> { + when (it.errorCode) { + BiometricPrompt.ERROR_LOCKOUT -> showBottomSheet(getAuthenticationTimer()) + //This event needs to be ignored to allow rotation and to allow user to send app to background and then foreground + BiometricPrompt.ERROR_CANCELED -> Unit + else -> view.closeCancel() + } + } + /*FingerprintResult.Fail happens when user fails authentication using, for example, a fingerprint that isn't associated yet + * Also, the Biometric library already shows a fail message withing the prompt.*/ + FingerprintResult.FAIL -> Unit + } + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleRetryAuthentication() { + disposables.add(view.getRetryButtonClick() + .observeOn(viewScheduler) + .doOnNext { + hasBottomsheetOn = false + view.closeCancel() + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun getAuthenticationTimer(): Long { + val lastAuthenticationErrorTime = fingerprintInteractor.getAuthenticationErrorTime() + val currentTime = System.currentTimeMillis() + return if (currentTime - lastAuthenticationErrorTime >= ERROR_RETRY_TIME_IN_MILLIS) { + fingerprintInteractor.setAuthenticationErrorTime(currentTime) + 30 + } else { + val time = + (ERROR_RETRY_TIME_IN_MILLIS - (currentTime - lastAuthenticationErrorTime)).toDouble() + ceil(time / 1000).toLong() + } + } + + fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(BOTTOMSHEET_KEY, hasBottomsheetOn) + } + + fun stop() = disposables.clear() + + fun onResume() { + //On resume to allow rotation and to allow the user to send the app to background and then to foregorund and keep the auth dialog + if (!hasBottomsheetOn) showBiometricPrompt() + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationPromptView.kt b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationPromptView.kt new file mode 100644 index 00000000000..4fb6e3852a6 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/AuthenticationPromptView.kt @@ -0,0 +1,23 @@ +package com.asfoundation.wallet.ui + +import androidx.biometric.BiometricPrompt +import io.reactivex.Observable + +interface AuthenticationPromptView { + + fun createBiometricPrompt(): BiometricPrompt + + fun getAuthenticationResult(): Observable + + fun showAuthenticationBottomSheet(timer: Long) + + fun showPrompt(biometricPrompt: BiometricPrompt, deviceCredentialsAllowed: Boolean) + + fun getRetryButtonClick(): Observable + + fun onRetryButtonClick() + + fun closeSuccess() + + fun closeCancel() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/BaseActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/BaseActivity.java index 115210b95b3..c6d74ea8634 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/BaseActivity.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/BaseActivity.java @@ -1,21 +1,31 @@ package com.asfoundation.wallet.ui; +import android.content.Intent; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.CollapsingToolbarLayout; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.text.SpannableString; import android.view.MenuItem; import android.view.Window; import android.view.WindowManager; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import com.asf.wallet.R; +import com.asfoundation.wallet.App; +import com.asfoundation.wallet.billing.analytics.PageViewAnalytics; +import com.asfoundation.wallet.util.KeyboardUtils; +import com.google.android.material.appbar.CollapsingToolbarLayout; +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.annotations.NotNull; -public abstract class BaseActivity extends AppCompatActivity { +public abstract class BaseActivity extends AppCompatActivity implements ActivityResultSharer { + + private List activityResultListeners; + private PageViewAnalytics pageViewAnalytics; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { + activityResultListeners = new ArrayList<>(); + pageViewAnalytics = new PageViewAnalytics(((App) getApplication()).analyticsManager()); super.onCreate(savedInstanceState); Window window = getWindow(); @@ -24,9 +34,10 @@ public abstract class BaseActivity extends AppCompatActivity { // add FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS flag to the window window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + } - // finally change the color - window.setStatusBarColor(ContextCompat.getColor(this,R.color.statusBarColor)); + protected void sendPageViewEvent() { + pageViewAnalytics.sendPageViewEvent(getClass().getSimpleName()); } protected Toolbar toolbar() { @@ -46,14 +57,7 @@ protected void setTitle(String title) { } } - protected void setSubtitle(String subtitle) { - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setSubtitle(subtitle); - } - } - - protected void setCollapsingTitle(SpannableString title) { + protected void setCollapsingTitle(String title) { CollapsingToolbarLayout collapsing = findViewById(R.id.toolbar_layout); if (collapsing != null) { collapsing.setTitle(title); @@ -74,26 +78,28 @@ protected void disableDisplayHomeAsUp() { } } - protected void hideToolbar() { - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.hide(); + @Override public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + KeyboardUtils.hideKeyboard(getWindow().getDecorView() + .getRootView()); + finish(); } + return true; } - protected void showToolbar() { - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.show(); - } + @Override public void addOnActivityListener(@NotNull ActivityResultListener listener) { + activityResultListeners.add(listener); } - @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - break; + @Override public void remove(@NotNull ActivityResultListener listener) { + activityResultListeners.remove(listener); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + for (ActivityResultListener listener : activityResultListeners) { + listener.onActivityResult(requestCode, resultCode, data); } - return true; } } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/ConfirmationActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/ConfirmationActivity.java index d35fdf09ec8..5b806a976ec 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/ConfirmationActivity.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/ConfirmationActivity.java @@ -1,26 +1,28 @@ package com.asfoundation.wallet.ui; import android.app.Activity; -import android.arch.lifecycle.ViewModelProviders; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AlertDialog; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.res.ResourcesCompat; +import androidx.lifecycle.ViewModelProviders; import com.asf.wallet.R; import com.asfoundation.wallet.C; import com.asfoundation.wallet.entity.ErrorEnvelope; import com.asfoundation.wallet.entity.PendingTransaction; import com.asfoundation.wallet.entity.TransactionBuilder; import com.asfoundation.wallet.util.BalanceUtils; +import com.asfoundation.wallet.util.CurrencyFormatUtils; +import com.asfoundation.wallet.util.WalletCurrency; import com.asfoundation.wallet.viewmodel.ConfirmationViewModel; import com.asfoundation.wallet.viewmodel.ConfirmationViewModelFactory; import com.asfoundation.wallet.viewmodel.GasSettingsViewModel; @@ -37,6 +39,7 @@ public class ConfirmationActivity extends BaseActivity { AlertDialog dialog; @Inject ConfirmationViewModelFactory confirmationViewModelFactory; + CurrencyFormatUtils currencyFormatUtils; ConfirmationViewModel viewModel; private TextView fromAddressText; private TextView toAddressText; @@ -52,7 +55,7 @@ public class ConfirmationActivity extends BaseActivity { setContentView(R.layout.activity_confirm); toolbar(); - + currencyFormatUtils = CurrencyFormatUtils.Companion.create(); fromAddressText = findViewById(R.id.text_from); toAddressText = findViewById(R.id.text_to); valueText = findViewById(R.id.text_value); @@ -74,21 +77,44 @@ public class ConfirmationActivity extends BaseActivity { .observe(this, this::onError); } + @Override public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_edit) { + viewModel.openGasSettings(ConfirmationActivity.this); + } + return super.onOptionsItemSelected(item); + } + + @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { + super.onActivityResult(requestCode, resultCode, intent); + if (requestCode == GasSettingsViewModel.SET_GAS_SETTINGS) { + if (resultCode == RESULT_OK) { + viewModel.setGasSettings(intent.getParcelableExtra(EXTRA_GAS_SETTINGS)); + } + } + } + private void onTransactionBuilder(TransactionBuilder transactionBuilder) { fromAddressText.setText(transactionBuilder.fromAddress()); toAddressText.setText(transactionBuilder.toAddress()); - valueText.setText(getString(R.string.new_transaction_value, transactionBuilder.amount(), - transactionBuilder.symbol())); - valueText.setTextColor(ContextCompat.getColor(this, R.color.red)); + String value = "-" + currencyFormatUtils.formatTransferCurrency(transactionBuilder.amount(), + WalletCurrency.ETHEREUM); + String symbol = transactionBuilder.symbol(); + int smallTitleSize = (int) getResources().getDimension(R.dimen.small_text); + int color = getResources().getColor(R.color.color_grey_9e); + valueText.setText(BalanceUtils.formatBalance(value, symbol, smallTitleSize, color)); BigDecimal gasPrice = transactionBuilder.gasSettings().gasPrice; BigDecimal gasLimit = transactionBuilder.gasSettings().gasLimit; - gasPriceText.setText( - getString(R.string.gas_price_value, BalanceUtils.weiToGwei(gasPrice), GWEI_UNIT)); + String formattedGasPrice = getString(R.string.gas_price_value, + currencyFormatUtils.formatTransferCurrency(BalanceUtils.weiToGwei(gasPrice), + WalletCurrency.ETHEREUM), GWEI_UNIT); + gasPriceText.setText(formattedGasPrice); gasLimitText.setText(transactionBuilder.gasSettings().gasLimit.toPlainString()); - String networkFee = BalanceUtils.weiToEth(gasPrice.multiply(gasLimit)) - .toPlainString() + " " + C.ETH_SYMBOL; + String networkFee = currencyFormatUtils.formatTransferCurrency( + BalanceUtils.weiToEth(gasPrice.multiply(gasLimit)), WalletCurrency.ETHEREUM) + + " " + + C.ETH_SYMBOL; networkFeeText.setText(networkFee); } @@ -98,21 +124,14 @@ private void onTransactionBuilder(TransactionBuilder transactionBuilder) { return super.onCreateOptionsMenu(menu); } - @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_edit: { - viewModel.openGasSettings(ConfirmationActivity.this); - } - break; - } - return super.onOptionsItemSelected(item); - } - private void onProgress(boolean shouldShowProgress) { if (shouldShowProgress) { hideDialog(); + ProgressBar progressBar = new ProgressBar(this); + progressBar.setIndeterminateDrawable( + ResourcesCompat.getDrawable(getResources(), R.drawable.gradient_progress, null)); dialog = new AlertDialog.Builder(this).setTitle(R.string.title_dialog_sending) - .setView(new ProgressBar(this)) + .setView(progressBar) .setCancelable(false) .create(); dialog.show(); @@ -132,6 +151,7 @@ private void onSend() { private void onTransaction(PendingTransaction transaction) { Log.d(TAG, "onTransaction() called with: transaction = [" + transaction + "]"); if (!transaction.isPending()) { + viewModel.progressFinished(); hideDialog(); dialog = new AlertDialog.Builder(this).setTitle(R.string.transaction_succeeded) .setMessage(transaction.getHash()) @@ -140,8 +160,11 @@ private void onTransaction(PendingTransaction transaction) { .setNeutralButton(R.string.copy, (dialog1, id) -> { ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("transaction transaction", transaction.getHash()); - clipboard.setPrimaryClip(clip); + if (clipboard != null) { + ClipData clip = + ClipData.newPlainText("transaction transaction", transaction.getHash()); + clipboard.setPrimaryClip(clip); + } successFinish(transaction.getHash()); }) .create(); @@ -167,17 +190,13 @@ private void onError(ErrorEnvelope error) { dialog.show(); } - @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { - if (requestCode == GasSettingsViewModel.SET_GAS_SETTINGS) { - if (resultCode == RESULT_OK) { - viewModel.setGasSettings(intent.getParcelableExtra(EXTRA_GAS_SETTINGS)); - } - } - } - @Override protected void onResume() { super.onResume(); - - viewModel.init(getIntent().getParcelableExtra(EXTRA_TRANSACTION_BUILDER)); + TransactionBuilder transactionBuilder = + getIntent().getParcelableExtra(EXTRA_TRANSACTION_BUILDER); + if (transactionBuilder != null) { + viewModel.init(transactionBuilder); + } + sendPageViewEvent(); } } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/EditTokensVisibilityActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/EditTokensVisibilityActivity.java deleted file mode 100644 index b82c5745cbb..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/EditTokensVisibilityActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.asfoundation.wallet.ui; - -import android.os.Bundle; -import android.support.annotation.Nullable; -import dagger.android.AndroidInjection; - -public class EditTokensVisibilityActivity extends BaseActivity { - - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { - AndroidInjection.inject(this); - super.onCreate(savedInstanceState); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/Erc681Receiver.java b/app/src/main/java/com/asfoundation/wallet/ui/Erc681Receiver.java deleted file mode 100644 index d3e84267144..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/Erc681Receiver.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.asfoundation.wallet.ui; - -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.Nullable; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.ui.iab.IabActivity; -import dagger.android.AndroidInjection; -import javax.inject.Inject; - -/** - * Created by trinkes on 13/03/2018. - */ - -public class Erc681Receiver extends BaseActivity { - - public static final int REQUEST_CODE = 234; - @Inject FindDefaultWalletInteract walletInteract; - - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { - AndroidInjection.inject(this); - super.onCreate(savedInstanceState); - walletInteract.find() - .subscribe(wallet -> startEipTransfer(), throwable -> startApp(throwable)); - } - - private void startApp(Throwable throwable) { - throwable.printStackTrace(); - startActivity(SplashActivity.newIntent(this)); - finish(); - } - - private void startEipTransfer() { - Intent intent; - if (getIntent().getData() - .toString() - .contains("/buy?")) { - intent = IabActivity.newIntent(this, getIntent()); - } else { - intent = SendActivity.newIntent(this, getIntent()); - } - startActivityForResult(intent, REQUEST_CODE); - } - - @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_CODE) { - setResult(resultCode, data); - finish(); - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/Erc681Receiver.kt b/app/src/main/java/com/asfoundation/wallet/ui/Erc681Receiver.kt new file mode 100644 index 00000000000..23e793936a9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/Erc681Receiver.kt @@ -0,0 +1,96 @@ +package com.asfoundation.wallet.ui + +import android.content.Intent +import android.os.Bundle +import android.view.View +import com.appcoins.wallet.bdsbilling.WalletService +import com.asf.wallet.R +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.ui.iab.IabActivity.Companion.PRODUCT_NAME +import com.asfoundation.wallet.ui.iab.IabActivity.Companion.newIntent +import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor +import com.asfoundation.wallet.util.TransferParser +import dagger.android.AndroidInjection +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.activity_iab_wallet_creation.* +import javax.inject.Inject + +/** + * Created by trinkes on 13/03/2018. + */ +class Erc681Receiver : BaseActivity(), Erc681ReceiverView { + @Inject + lateinit var walletService: WalletService + + @Inject + lateinit var transferParser: TransferParser + + @Inject + lateinit var inAppPurchaseInteractor: InAppPurchaseInteractor + private lateinit var presenter: Erc681ReceiverPresenter + + + companion object { + const val REQUEST_CODE = 234 + } + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_iab_wallet_creation) + val productName = intent.extras!!.getString(PRODUCT_NAME, "") + presenter = + Erc681ReceiverPresenter(this, transferParser, inAppPurchaseInteractor, walletService, + intent.dataString!!, + AndroidSchedulers.mainThread(), CompositeDisposable(), productName) + presenter.present(savedInstanceState) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, + data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE) { + setResult(resultCode, data) + finish() + } + } + + override fun startEipTransfer(transaction: TransactionBuilder, isBds: Boolean, + developerPayload: String?) { + val intent: Intent = if (intent.data != null && intent.data.toString() + .contains("/buy?")) { + newIntent(this, intent, transaction, isBds, developerPayload) + } else { + SendActivity.newIntent(this, intent) + } + startActivityForResult(intent, REQUEST_CODE) + } + + override fun startApp(throwable: Throwable) { + throwable.printStackTrace() + startActivity(SplashActivity.newIntent(this)) + finish() + } + + override fun endAnimation() { + create_wallet_animation?.visibility = View.INVISIBLE + create_wallet_text?.visibility = View.INVISIBLE + create_wallet_card?.visibility = View.INVISIBLE + create_wallet_animation?.removeAllAnimatorListeners() + create_wallet_animation?.removeAllUpdateListeners() + create_wallet_animation?.removeAllLottieOnCompositionLoadedListener() + } + + override fun showLoadingAnimation() { + create_wallet_animation?.visibility = View.VISIBLE + create_wallet_card?.visibility = View.VISIBLE + create_wallet_text?.visibility = View.VISIBLE + create_wallet_animation?.playAnimation() + } + + override fun onPause() { + presenter.pause() + super.onPause() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/Erc681ReceiverPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/Erc681ReceiverPresenter.kt new file mode 100644 index 00000000000..13642d53331 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/Erc681ReceiverPresenter.kt @@ -0,0 +1,72 @@ +package com.asfoundation.wallet.ui + +import android.os.Bundle +import com.appcoins.wallet.bdsbilling.WalletService +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.service.WalletGetterStatus +import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor +import com.asfoundation.wallet.util.TransferParser +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +internal class Erc681ReceiverPresenter(private val view: Erc681ReceiverView, + private val transferParser: TransferParser, + private val inAppPurchaseInteractor: InAppPurchaseInteractor, + private val walletService: WalletService, + private val data: String, + private val viewScheduler: Scheduler, + private var disposables: CompositeDisposable, + private val productName: String?) { + fun present(savedInstanceState: Bundle?) { + if (savedInstanceState == null) { + disposables.add( + handleWalletCreationIfNeeded() + .takeUntil { it != WalletGetterStatus.CREATING.toString() } + .flatMap { + transferParser.parse(data) + .map { transactionBuilder: TransactionBuilder -> + var callingPackage = transactionBuilder.domain + if (callingPackage == null) { + callingPackage = view.callingPackage + } + transactionBuilder.domain = callingPackage + transactionBuilder.productName = productName + transactionBuilder + } + .flatMap { transactionBuilder: TransactionBuilder -> + inAppPurchaseInteractor.isWalletFromBds( + transactionBuilder.domain, transactionBuilder.toAddress()) + .doOnSuccess { isBds: Boolean? -> + view.startEipTransfer(transactionBuilder, isBds, + transactionBuilder.payload) + } + } + .toObservable() + + } + .subscribe({ }, { throwable: Throwable? -> view.startApp(throwable) }) + ) + } + } + + private fun handleWalletCreationIfNeeded(): Observable { + return walletService.findWalletOrCreate() + .observeOn(viewScheduler) + .doOnNext { + if (it == WalletGetterStatus.CREATING.toString()) { + view.showLoadingAnimation() + } + } + .filter { it != WalletGetterStatus.CREATING.toString() } + .map { + view.endAnimation() + it + } + } + + fun pause() { + disposables.clear() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/Erc681ReceiverView.java b/app/src/main/java/com/asfoundation/wallet/ui/Erc681ReceiverView.java new file mode 100644 index 00000000000..85a2383aafe --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/Erc681ReceiverView.java @@ -0,0 +1,16 @@ +package com.asfoundation.wallet.ui; + +import com.asfoundation.wallet.entity.TransactionBuilder; + +interface Erc681ReceiverView { + String getCallingPackage(); + + void startEipTransfer(TransactionBuilder transactionBuilder, Boolean isBds, + String developerPayload); + + void startApp(Throwable throwable); + + void endAnimation(); + + void showLoadingAnimation(); +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/FingerPrintInteractor.kt b/app/src/main/java/com/asfoundation/wallet/ui/FingerPrintInteractor.kt new file mode 100644 index 00000000000..3893a3ac105 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/FingerPrintInteractor.kt @@ -0,0 +1,41 @@ +package com.asfoundation.wallet.ui + +import android.content.pm.PackageManager +import android.content.pm.PackageManager.FEATURE_FINGERPRINT +import android.os.Build +import androidx.biometric.BiometricManager +import com.asfoundation.wallet.repository.PreferencesRepositoryType + +class FingerPrintInteractor(private val biometricManager: BiometricManager, + private val packageManager: PackageManager, + private val preferencesRepositoryType: PreferencesRepositoryType) { + + fun getDeviceCompatibility(): Int { + val biometricCompatibility = biometricManager.canAuthenticate() + //User may have biometrics but no fingerprint (e.g face recognition) + if (hasBiometrics(biometricCompatibility) && !hasFingerPrint()) { + return BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE + } + return biometricCompatibility + } + + fun setAuthenticationPermission(value: Boolean) = + preferencesRepositoryType.setAuthenticationPermission(value) + + fun getAuthenticationErrorTime() = preferencesRepositoryType.getAuthenticationErrorTime() + + fun setAuthenticationErrorTime(currentTime: Long) = + preferencesRepositoryType.setAuthenticationErrorTime(currentTime) + + private fun hasBiometrics(biometricCompatibility: Int): Boolean { + return (biometricCompatibility == BiometricManager.BIOMETRIC_SUCCESS || biometricCompatibility == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) + } + + private fun hasFingerPrint(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + packageManager.hasSystemFeature(FEATURE_FINGERPRINT) + } else { + false + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/FingerprintAuthResult.kt b/app/src/main/java/com/asfoundation/wallet/ui/FingerprintAuthResult.kt new file mode 100644 index 00000000000..083336de62f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/FingerprintAuthResult.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.ui + +import androidx.biometric.BiometricPrompt + + +data class FingerprintAuthResult(val errorCode: Int?, + val errorString: String?, + val result: BiometricPrompt.AuthenticationResult?, + val type: FingerprintResult) + +enum class FingerprintResult { + SUCCESS, ERROR, FAIL +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/GasSettingsActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/GasSettingsActivity.java index c2563e8ecf5..e9a4fb2c62b 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/GasSettingsActivity.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/GasSettingsActivity.java @@ -1,18 +1,20 @@ package com.asfoundation.wallet.ui; -import android.arch.lifecycle.ViewModelProviders; import android.content.Intent; import android.os.Bundle; -import android.support.annotation.Nullable; import android.view.Menu; import android.view.MenuItem; import android.widget.SeekBar; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProviders; import com.asf.wallet.R; import com.asfoundation.wallet.C; import com.asfoundation.wallet.entity.GasSettings; import com.asfoundation.wallet.entity.NetworkInfo; import com.asfoundation.wallet.util.BalanceUtils; +import com.asfoundation.wallet.util.CurrencyFormatUtils; +import com.asfoundation.wallet.util.WalletCurrency; import com.asfoundation.wallet.viewmodel.GasSettingsViewModel; import com.asfoundation.wallet.viewmodel.GasSettingsViewModelFactory; import dagger.android.AndroidInjection; @@ -24,7 +26,7 @@ public class GasSettingsActivity extends BaseActivity { @Inject GasSettingsViewModelFactory viewModelFactory; GasSettingsViewModel viewModel; - + private CurrencyFormatUtils currencyFormatUtils; private TextView gasPriceText; private TextView gasLimitText; private TextView networkFeeText; @@ -39,6 +41,7 @@ public class GasSettingsActivity extends BaseActivity { setContentView(R.layout.activity_gas_settings); toolbar(); + currencyFormatUtils = CurrencyFormatUtils.Companion.create(); SeekBar gasPriceSlider = findViewById(R.id.gas_price_slider); SeekBar gasLimitSlider = findViewById(R.id.gas_limit_slider); gasPriceText = findViewById(R.id.gas_price_text); @@ -116,11 +119,24 @@ public class GasSettingsActivity extends BaseActivity { .setValue(gasLimit); } + @Override public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_save) { + Intent intent = new Intent(); + intent.putExtra(C.EXTRA_GAS_SETTINGS, new GasSettings(new BigDecimal(viewModel.gasPrice() + .getValue()), new BigDecimal(viewModel.gasLimit() + .getValue()))); + setResult(RESULT_OK, intent); + finish(); + } + return super.onOptionsItemSelected(item); + } + @Override public void onResume() { super.onResume(); viewModel.prepare(); + sendPageViewEvent(); } private void onDefaultNetwork(NetworkInfo network) { @@ -131,8 +147,12 @@ private void onDefaultNetwork(NetworkInfo network) { } private void onGasPrice(BigInteger price) { - String priceStr = BalanceUtils.weiToGwei(new BigDecimal(price)) + " " + C.GWEI_UNIT; - gasPriceText.setText(priceStr); + BigDecimal priceStr = BalanceUtils.weiToGwei(new BigDecimal(price)); + String formattedPrice = + currencyFormatUtils.formatTransferCurrency(priceStr, WalletCurrency.ETHEREUM) + + " " + + C.GWEI_UNIT; + gasPriceText.setText(formattedPrice); updateNetworkFee(); } @@ -144,9 +164,11 @@ private void onGasLimit(BigInteger limit) { } private void updateNetworkFee() { - String fee = BalanceUtils.weiToEth(viewModel.networkFee()) - .toPlainString() + " " + C.ETH_SYMBOL; - networkFeeText.setText(fee); + BigDecimal fee = BalanceUtils.weiToEth(viewModel.networkFee()); + String formattedFee = currencyFormatUtils.formatTransferCurrency(fee, WalletCurrency.ETHEREUM) + + " " + + C.ETH_SYMBOL; + networkFeeText.setText(formattedFee); } @Override public boolean onCreateOptionsMenu(Menu menu) { @@ -154,19 +176,4 @@ private void updateNetworkFee() { return super.onCreateOptionsMenu(menu); } - - @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_save: { - Intent intent = new Intent(); - intent.putExtra(C.EXTRA_GAS_SETTINGS, new GasSettings(new BigDecimal(viewModel.gasPrice() - .getValue()), new BigDecimal(viewModel.gasLimit() - .getValue()))); - setResult(RESULT_OK, intent); - finish(); - } - break; - } - return super.onOptionsItemSelected(item); - } } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/ImportKeystoreFragment.java b/app/src/main/java/com/asfoundation/wallet/ui/ImportKeystoreFragment.java deleted file mode 100644 index 845e55a70c6..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/ImportKeystoreFragment.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.asfoundation.wallet.ui; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import com.asf.wallet.R; -import com.asfoundation.wallet.ui.widget.OnImportKeystoreListener; - -public class ImportKeystoreFragment extends Fragment implements View.OnClickListener { - - private static final OnImportKeystoreListener dummyOnImportKeystoreListener = (k, p) -> { - }; - - private EditText keystore; - private EditText password; - @NonNull private OnImportKeystoreListener onImportKeystoreListener = - dummyOnImportKeystoreListener; - - public static ImportKeystoreFragment create() { - return new ImportKeystoreFragment(); - } - - @Nullable @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - return LayoutInflater.from(getContext()) - .inflate(R.layout.fragment_import_keystore, container, false); - } - - @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - keystore = view.findViewById(R.id.keystore); - password = view.findViewById(R.id.password); - view.findViewById(R.id.import_action) - .setOnClickListener(this); - } - - @Override public void onClick(View view) { - this.keystore.setError(null); - String keystore = this.keystore.getText() - .toString(); - String password = this.password.getText() - .toString(); - if (TextUtils.isEmpty(keystore)) { - this.keystore.setError(getString(R.string.error_field_required)); - } else { - onImportKeystoreListener.onKeystore(keystore, password); - } - } - - public void setOnImportKeystoreListener( - @Nullable OnImportKeystoreListener onImportKeystoreListener) { - this.onImportKeystoreListener = - onImportKeystoreListener == null ? dummyOnImportKeystoreListener : onImportKeystoreListener; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/ImportPrivateKeyFragment.java b/app/src/main/java/com/asfoundation/wallet/ui/ImportPrivateKeyFragment.java deleted file mode 100644 index ad37840c910..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/ImportPrivateKeyFragment.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.asfoundation.wallet.ui; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import com.asf.wallet.R; -import com.asfoundation.wallet.ui.widget.OnImportPrivateKeyListener; - -public class ImportPrivateKeyFragment extends Fragment implements View.OnClickListener { - - private static final OnImportPrivateKeyListener dummyOnImportPrivateKeyListener = key -> { - }; - - private EditText privateKey; - private OnImportPrivateKeyListener onImportPrivateKeyListener = dummyOnImportPrivateKeyListener; - - public static ImportPrivateKeyFragment create() { - return new ImportPrivateKeyFragment(); - } - - @Nullable @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - return LayoutInflater.from(getContext()) - .inflate(R.layout.fragment_import_private_key, container, false); - } - - @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - privateKey = view.findViewById(R.id.private_key); - view.findViewById(R.id.import_action) - .setOnClickListener(this); - } - - @Override public void onClick(View view) { - privateKey.setError(null); - String value = privateKey.getText() - .toString(); - if (TextUtils.isEmpty(value) || value.length() != 64) { - privateKey.setError(getString(R.string.error_field_required)); - } else { - onImportPrivateKeyListener.onPrivateKey(privateKey.getText() - .toString()); - } - } - - public void setOnImportPrivateKeyListener(OnImportPrivateKeyListener onImportPrivateKeyListener) { - this.onImportPrivateKeyListener = - onImportPrivateKeyListener == null ? dummyOnImportPrivateKeyListener - : onImportPrivateKeyListener; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/ImportWalletActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/ImportWalletActivity.java deleted file mode 100644 index bd173d9075a..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/ImportWalletActivity.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.asfoundation.wallet.ui; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.arch.lifecycle.ViewModelProviders; -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.TabLayout; -import android.support.v4.app.Fragment; -import android.support.v4.util.Pair; -import android.support.v4.view.ViewPager; -import android.text.TextUtils; -import android.widget.ProgressBar; -import com.asf.wallet.R; -import com.asfoundation.wallet.C; -import com.asfoundation.wallet.entity.ErrorEnvelope; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.ui.widget.adapter.TabPagerAdapter; -import com.asfoundation.wallet.viewmodel.ImportWalletViewModel; -import com.asfoundation.wallet.viewmodel.ImportWalletViewModelFactory; -import dagger.android.AndroidInjection; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; - -import static com.asfoundation.wallet.C.ErrorCode.ALREADY_ADDED; - -public class ImportWalletActivity extends BaseActivity { - - private static final int KEYSTORE_FORM_INDEX = 0; - private static final int PRIVATE_KEY_FORM_INDEX = 1; - - private final List> pages = new ArrayList<>(); - - @Inject ImportWalletViewModelFactory importWalletViewModelFactory; - ImportWalletViewModel importWalletViewModel; - private Dialog dialog; - - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { - AndroidInjection.inject(this); - - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_import_wallet); - toolbar(); - - pages.add(KEYSTORE_FORM_INDEX, - new Pair<>(getString(R.string.tab_keystore), ImportKeystoreFragment.create())); - pages.add(PRIVATE_KEY_FORM_INDEX, - new Pair<>(getString(R.string.tab_private_key), ImportPrivateKeyFragment.create())); - ViewPager viewPager = findViewById(R.id.viewPager); - viewPager.setAdapter(new TabPagerAdapter(getSupportFragmentManager(), pages)); - TabLayout tabLayout = findViewById(R.id.tabLayout); - tabLayout.setupWithViewPager(viewPager); - - importWalletViewModel = ViewModelProviders.of(this, importWalletViewModelFactory) - .get(ImportWalletViewModel.class); - importWalletViewModel.progress() - .observe(this, this::onProgress); - importWalletViewModel.error() - .observe(this, this::onError); - importWalletViewModel.wallet() - .observe(this, this::onWallet); - } - - private void onWallet(Wallet wallet) { - Intent result = new Intent(); - result.putExtra(C.Key.WALLET, wallet); - setResult(RESULT_OK, result); - finish(); - } - - @Override public void onBackPressed() { - setResult(RESULT_CANCELED); - super.onBackPressed(); - } - - @Override protected void onPause() { - super.onPause(); - - hideDialog(); - } - - @Override protected void onResume() { - super.onResume(); - - ((ImportKeystoreFragment) pages.get(KEYSTORE_FORM_INDEX).second).setOnImportKeystoreListener( - importWalletViewModel); - ((ImportPrivateKeyFragment) pages.get( - PRIVATE_KEY_FORM_INDEX).second).setOnImportPrivateKeyListener(importWalletViewModel); - } - - private void onError(ErrorEnvelope errorEnvelope) { - hideDialog(); - String message = TextUtils.isEmpty(errorEnvelope.message) ? getString(R.string.error_import) - : errorEnvelope.message; - if (errorEnvelope.code == ALREADY_ADDED) { - message = getString(R.string.error_already_added); - } - dialog = new AlertDialog.Builder(this).setTitle(R.string.title_dialog_error) - .setMessage(message) - .setPositiveButton(R.string.ok, null) - .create(); - dialog.show(); - } - - private void onProgress(boolean shouldShowProgress) { - hideDialog(); - if (shouldShowProgress) { - dialog = new AlertDialog.Builder(this).setTitle(R.string.title_dialog_handling) - .setView(new ProgressBar(this)) - .setCancelable(false) - .create(); - dialog.show(); - } - } - - private void hideDialog() { - if (dialog != null && dialog.isShowing()) { - dialog.dismiss(); - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/MyAddressActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/MyAddressActivity.java index d635ca8a884..d2bb4336fc4 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/MyAddressActivity.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/MyAddressActivity.java @@ -6,15 +6,18 @@ import android.graphics.Bitmap; import android.graphics.Point; import android.os.Bundle; -import android.support.annotation.Nullable; +import android.view.MenuItem; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProviders; import com.asf.wallet.R; import com.asfoundation.wallet.entity.NetworkInfo; import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; +import com.asfoundation.wallet.viewmodel.MyAddressViewModel; +import com.asfoundation.wallet.viewmodel.MyAddressViewModelFactory; import com.google.zxing.BarcodeFormat; import com.google.zxing.MultiFormatWriter; import com.google.zxing.common.BitMatrix; @@ -28,7 +31,9 @@ public class MyAddressActivity extends BaseActivity implements View.OnClickListe public static final String KEY_ADDRESS = "key_address"; private static final float QR_IMAGE_WIDTH_RATIO = 0.9f; - @Inject protected EthereumNetworkRepositoryType ethereumNetworkRepository; + @Inject protected NetworkInfo defaultNetwork; + @Inject MyAddressViewModelFactory myAddressViewModelFactory; + MyAddressViewModel viewModel; private Wallet wallet; @@ -41,9 +46,10 @@ public class MyAddressActivity extends BaseActivity implements View.OnClickListe toolbar(); + viewModel = ViewModelProviders.of(this, myAddressViewModelFactory) + .get(MyAddressViewModel.class); wallet = getIntent().getParcelableExtra(WALLET); - NetworkInfo networkInfo = ethereumNetworkRepository.getDefaultNetwork(); - String suggestion = getString(R.string.suggestion_this_is_your_address, networkInfo.name); + String suggestion = getString(R.string.suggestion_this_is_your_address, defaultNetwork.name); ((TextView) findViewById(R.id.address_suggestion)).setText(suggestion); ((TextView) findViewById(R.id.address)).setText(wallet.address); findViewById(R.id.copy_action).setOnClickListener(this); @@ -51,6 +57,18 @@ public class MyAddressActivity extends BaseActivity implements View.OnClickListe ((ImageView) findViewById(R.id.qr_image)).setImageBitmap(qrCode); } + @Override public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + viewModel.showTransactions(this); + } + return super.onOptionsItemSelected(item); + } + + @Override protected void onResume() { + super.onResume(); + sendPageViewEvent(); + } + private Bitmap createQRImage(String address) { Point size = new Point(); getWindowManager().getDefaultDisplay() @@ -78,4 +96,8 @@ private Bitmap createQRImage(String address) { Toast.makeText(this, "Copied to clipboard", Toast.LENGTH_SHORT) .show(); } + + @Override public void onBackPressed() { + viewModel.showTransactions(this); + } } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/OneStepPaymentReceiver.kt b/app/src/main/java/com/asfoundation/wallet/ui/OneStepPaymentReceiver.kt new file mode 100644 index 00000000000..8e3fd516be0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/OneStepPaymentReceiver.kt @@ -0,0 +1,132 @@ +package com.asfoundation.wallet.ui + +import android.content.Intent +import android.os.Bundle +import android.view.View +import com.airbnb.lottie.LottieAnimationView +import com.appcoins.wallet.bdsbilling.WalletService +import com.asf.wallet.R +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.service.WalletGetterStatus +import com.asfoundation.wallet.ui.iab.IabActivity +import com.asfoundation.wallet.ui.iab.IabActivity.Companion.newIntent +import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor +import com.asfoundation.wallet.util.TransferParser +import dagger.android.AndroidInjection +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import javax.inject.Inject + +class OneStepPaymentReceiver : BaseActivity() { + @Inject + lateinit var inAppPurchaseInteractor: InAppPurchaseInteractor + + @Inject + lateinit var walletService: WalletService + + @Inject + lateinit var transferParser: TransferParser + private var disposable: Disposable? = null + private var walletCreationCard: View? = null + private var walletCreationAnimation: LottieAnimationView? = null + private var walletCreationText: View? = null + + companion object { + const val REQUEST_CODE = 234 + } + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_iab_wallet_creation) + walletCreationCard = findViewById(R.id.create_wallet_card) + walletCreationAnimation = + findViewById(R.id.create_wallet_animation) + walletCreationText = findViewById(R.id.create_wallet_text) + if (savedInstanceState == null) { + disposable = handleWalletCreationIfNeeded() + .takeUntil { it != WalletGetterStatus.CREATING.toString() } + .flatMap { + transferParser.parse(intent.dataString!!) + .flatMap { transaction: TransactionBuilder -> + inAppPurchaseInteractor.isWalletFromBds(transaction.domain, + transaction.toAddress()) + .doOnSuccess { isBds: Boolean -> + startOneStepTransfer(transaction, isBds) + } + } + .toObservable() + } + .subscribe({ }, { throwable: Throwable -> startApp(throwable) }) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, + data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE) { + setResult(resultCode, data) + finish() + } + } + + private fun startApp(throwable: Throwable) { + throwable.printStackTrace() + startActivity(SplashActivity.newIntent(this)) + finish() + } + + private fun startOneStepTransfer(transaction: TransactionBuilder, + isBds: Boolean) { + val intent = + newIntent(this, intent, transaction, isBds, transaction.payload) + intent.putExtra(IabActivity.PRODUCT_NAME, transaction.skuId) + startActivityForResult(intent, REQUEST_CODE) + } + + override fun onPause() { + if (disposable != null && !disposable!!.isDisposed) { + disposable!!.dispose() + } + super.onPause() + } + + override fun onDestroy() { + super.onDestroy() + walletCreationCard = null + walletCreationAnimation = null + walletCreationText = null + } + + private fun handleWalletCreationIfNeeded(): Observable { + return walletService.findWalletOrCreate() + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { + if (it == WalletGetterStatus.CREATING.toString()) { + showLoadingAnimation() + } + } + .filter { it != WalletGetterStatus.CREATING.toString() } + .map { + endAnimation() + it + } + } + + private fun endAnimation() { + walletCreationAnimation!!.visibility = View.INVISIBLE + walletCreationCard!!.visibility = View.INVISIBLE + walletCreationText!!.visibility = View.INVISIBLE + walletCreationAnimation!!.removeAllAnimatorListeners() + walletCreationAnimation!!.removeAllUpdateListeners() + walletCreationAnimation!!.removeAllLottieOnCompositionLoadedListener() + } + + private fun showLoadingAnimation() { + walletCreationAnimation!!.visibility = View.VISIBLE + walletCreationCard!!.visibility = View.VISIBLE + walletCreationText!!.visibility = View.VISIBLE + walletCreationAnimation!!.playAnimation() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/OverlayFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/OverlayFragment.kt new file mode 100644 index 00000000000..cfad626d88b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/OverlayFragment.kt @@ -0,0 +1,132 @@ +package com.asfoundation.wallet.ui + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.INVISIBLE +import android.view.ViewGroup +import android.view.ViewTreeObserver +import androidx.fragment.app.Fragment +import com.asf.wallet.R +import com.google.android.material.bottomnavigation.BottomNavigationItemView +import com.google.android.material.bottomnavigation.BottomNavigationMenuView +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.overlay_fragment.* + + +class OverlayFragment : Fragment(), OverlayView { + + private lateinit var presenter: OverlayPresenter + private lateinit var activity: TransactionsActivity + + private val item: Int by lazy { + if (arguments!!.containsKey(ITEM_KEY)) { + arguments!!.getInt(ITEM_KEY) + } else { + throw IllegalArgumentException("item not found") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = OverlayPresenter(this, CompositeDisposable()) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + require( + context is TransactionsActivity) { OverlayFragment::class.java.simpleName + " needs to be attached to a " + TransactionsActivity::class.java.simpleName } + activity = context + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + handleItemAndArrowPosition() + presenter.present() + } + + private fun handleItemAndArrowPosition() { + //Highlights the correct BN item + val size = overlay_bottom_navigation.menu.size() + for (i in 0 until size) { + if (i != item) { + overlay_bottom_navigation.menu.getItem(i) + .icon = null + overlay_bottom_navigation.menu.getItem(i) + .title = "" + } + } + //If selected view is not on the first half of the Bottom Navigation hide arrow + if (item > size / 2) { + arrow_down_tip.visibility = INVISIBLE + } else { + setArrowPosition() + } + + } + + private fun setArrowPosition() { + val bottomNavigationMenuView = (overlay_bottom_navigation as BottomNavigationView) + .getChildAt(0) as BottomNavigationMenuView + val promotionsIcon = bottomNavigationMenuView.getChildAt(item) + val itemView = promotionsIcon as BottomNavigationItemView + val icon = itemView.getChildAt(1) + icon.viewTreeObserver.addOnGlobalLayoutListener( + object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + icon.viewTreeObserver.removeOnGlobalLayoutListener(this) + val location = IntArray(2) + icon.getLocationInWindow(location) + arrow_down_tip.x = + location[0] * 1f + (icon.width / 4f) + (arrow_down_tip.width / 4f) + } + }) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.overlay_fragment, container, false) + } + + override fun discoverClick(): Observable { + return RxView.clicks(discover_button) + } + + override fun dismissClick(): Observable { + return RxView.clicks(dismiss_button) + } + + override fun dismissView() { + activity.onBackPressed() + } + + override fun overlayClick(): Observable { + return RxView.clicks(overlay_container) + } + + override fun navigateToPromotions() { + activity.navigateToPromotions(true) + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + companion object { + private const val ITEM_KEY = "item" + + @JvmStatic + fun newInstance(highlightedBottomNavigationItem: Int): Fragment { + val fragment = OverlayFragment() + val bundle = Bundle() + bundle.putInt(ITEM_KEY, highlightedBottomNavigationItem) + fragment.arguments = bundle + return fragment + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/OverlayPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/OverlayPresenter.kt new file mode 100644 index 00000000000..adf78b97ff8 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/OverlayPresenter.kt @@ -0,0 +1,28 @@ +package com.asfoundation.wallet.ui + +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable + +class OverlayPresenter(private val view: OverlayView, private val disposable: CompositeDisposable) { + + fun present() { + handleDismissClick() + handleDiscoverClick() + } + + private fun handleDiscoverClick() { + disposable.add(view.discoverClick() + .doOnNext { view.navigateToPromotions() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleDismissClick() { + disposable.add(Observable.merge(view.dismissClick(), view.overlayClick()) + .doOnNext { view.dismissView() } + .subscribe({}, { it.printStackTrace() })) + } + + fun stop() { + disposable.clear() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/OverlayView.kt b/app/src/main/java/com/asfoundation/wallet/ui/OverlayView.kt new file mode 100644 index 00000000000..adfe515be79 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/OverlayView.kt @@ -0,0 +1,11 @@ +package com.asfoundation.wallet.ui + +import io.reactivex.Observable + +interface OverlayView { + fun discoverClick(): Observable + fun dismissClick(): Observable + fun navigateToPromotions() + fun dismissView() + fun overlayClick(): Observable +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/PaymentNavigationData.kt b/app/src/main/java/com/asfoundation/wallet/ui/PaymentNavigationData.kt new file mode 100644 index 00000000000..143de2e82eb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/PaymentNavigationData.kt @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.ui + +import java.io.Serializable + + +data class PaymentNavigationData(val paymentId: String, val paymentLabel: String, + val paymentIconUrl: String, val isPreselected: Boolean) : + Serializable diff --git a/app/src/main/java/com/asfoundation/wallet/ui/RxCheckbox.java b/app/src/main/java/com/asfoundation/wallet/ui/RxCheckbox.java new file mode 100644 index 00000000000..daa598e8dca --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/RxCheckbox.java @@ -0,0 +1,68 @@ +package com.asfoundation.wallet.ui; + +import android.os.Looper; +import androidx.annotation.CheckResult; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import io.reactivex.Observable; +import io.reactivex.Observer; +import io.reactivex.android.MainThreadDisposable; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposables; + +public class RxCheckbox extends Observable { + private final CheckBox view; + + public RxCheckbox(CheckBox view) { + this.view = view; + } + + @CheckResult @NonNull public static Observable checks(@NonNull CheckBox view) { + if (view == null) { + throw new NullPointerException("view == null"); + } + return new RxCheckbox(view); + } + + @Override protected void subscribeActual(Observer observer) { + if (!checkMainThread(observer)) { + return; + } + Listener listener = new Listener(view, observer); + observer.onSubscribe(listener); + view.setOnCheckedChangeListener(listener); + } + + private boolean checkMainThread(Observer observer) { + if (Looper.myLooper() != Looper.getMainLooper()) { + observer.onSubscribe(Disposables.empty()); + observer.onError(new IllegalStateException( + "Expected to be called on the main thread but was " + Thread.currentThread() + .getName())); + return false; + } + return true; + } + + static final class Listener extends MainThreadDisposable + implements CompoundButton.OnCheckedChangeListener { + private final CheckBox view; + + private final Observer observer; + + Listener(CheckBox view, Observer observer) { + this.view = view; + this.observer = observer; + } + + @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (!isDisposed()) { + observer.onNext(isChecked); + } + } + + @Override protected void onDispose() { + view.setOnCheckedChangeListener(null); + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SendActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/SendActivity.java index ce1e1752847..30718c5eabd 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/SendActivity.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/SendActivity.java @@ -1,18 +1,17 @@ package com.asfoundation.wallet.ui; import android.app.Activity; -import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.TextInputLayout; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.widget.EditText; import android.widget.ImageButton; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProviders; import com.asf.wallet.R; import com.asfoundation.wallet.router.Result; import com.asfoundation.wallet.ui.barcode.BarcodeCaptureActivity; @@ -21,6 +20,7 @@ import com.asfoundation.wallet.viewmodel.SendViewModelFactory; import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.vision.barcode.Barcode; +import com.google.android.material.textfield.TextInputLayout; import dagger.android.AndroidInjection; import java.math.BigDecimal; import java.text.NumberFormat; @@ -33,7 +33,7 @@ public class SendActivity extends BaseActivity { private static final int BARCODE_READER_REQUEST_CODE = 1; @Inject SendViewModelFactory sendViewModelFactory; - SendViewModel viewModel; + private SendViewModel viewModel; private EditText toAddressText; private EditText amountText; private TextInputLayout toInputLayout; @@ -48,6 +48,11 @@ public static Intent newIntent(Context context, Intent previousIntent) { return intent; } + @Override protected void onResume() { + super.onResume(); + sendPageViewEvent(); + } + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { AndroidInjection.inject(this); @@ -55,7 +60,6 @@ public static Intent newIntent(Context context, Intent previousIntent) { setContentView(R.layout.activity_send); toolbar(); - toInputLayout = findViewById(R.id.to_input_layout); toAddressText = findViewById(R.id.send_to_address); amountInputLayout = findViewById(R.id.amount_input_layout); @@ -82,30 +86,16 @@ public static Intent newIntent(Context context, Intent previousIntent) { }); } - private void onFinishWithResult(Result result) { - if (result.isSuccess()) { - setResult(Activity.RESULT_OK, result.getData()); - finish(); - } - } - - private void onAmount(BigDecimal bigDecimal) { - amountText.setText(NumberFormat.getInstance() - .format(bigDecimal)); - } - - @Override public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.send_menu, menu); - - return super.onCreateOptionsMenu(menu); - } - @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_next: { onNext(); + break; + } + case android.R.id.home: { + viewModel.showTransactions(this); + break; } - break; } return super.onOptionsItemSelected(item); } @@ -130,6 +120,24 @@ private void onAmount(BigDecimal bigDecimal) { } } + private void onFinishWithResult(Result result) { + if (result.isSuccess()) { + setResult(Activity.RESULT_OK, result.getData()); + finish(); + } + } + + private void onAmount(BigDecimal bigDecimal) { + amountText.setText(NumberFormat.getInstance() + .format(bigDecimal)); + } + + @Override public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.send_menu, menu); + + return super.onCreateOptionsMenu(menu); + } + private void onNext() { // Validate input fields boolean hasError = false; @@ -151,7 +159,6 @@ private void onNext() { if (!hasError) { toInputLayout.setErrorEnabled(false); amountInputLayout.setErrorEnabled(false); - viewModel.openConfirmation(this); } } @@ -162,7 +169,13 @@ private void onToAddress(String toAddress) { } private void onSymbol(String symbol) { - setTitle(getString(R.string.title_send) + " " + symbol); - amountInputLayout.setHint(getString(R.string.hint_amount) + " " + symbol); + if (symbol != null) { + setTitle(String.format(getString(R.string.title_send_with_token), symbol)); + amountInputLayout.setHint(String.format(getString(R.string.hint_amount_with_token), symbol)); + } + } + + @Override public void onBackPressed() { + viewModel.showTransactions(this); } } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/SettingsActivity.java deleted file mode 100644 index c239f196c7e..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/SettingsActivity.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.asfoundation.wallet.ui; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.MenuItem; -import com.asf.wallet.R; -import com.asfoundation.wallet.router.TransactionsRouter; -import dagger.android.AndroidInjection; -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.HasFragmentInjector; -import javax.inject.Inject; - -public class SettingsActivity extends BaseActivity implements HasFragmentInjector { - - @Inject DispatchingAndroidInjector fragmentInjector; - - @Override protected void onCreate(Bundle savedInstanceState) { - AndroidInjection.inject(this); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_settings); - toolbar(); - // Display the fragment as the main content. - getFragmentManager().beginTransaction() - .replace(R.id.fragment_container, new SettingsFragment()) - .commit(); - } - - @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: { - new TransactionsRouter().open(this, true); - finish(); - return true; - } - } - return super.onOptionsItemSelected(item); - } - - @Override public void onBackPressed() { - new TransactionsRouter().open(this, true); - finish(); - } - - @Override public AndroidInjector fragmentInjector() { - return fragmentInjector; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/SettingsActivity.kt new file mode 100644 index 00000000000..cf139151152 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SettingsActivity.kt @@ -0,0 +1,86 @@ +package com.asfoundation.wallet.ui + +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import com.asf.wallet.R +import com.asfoundation.wallet.router.TransactionsRouter +import com.asfoundation.wallet.ui.backup.WalletBackupActivity +import com.asfoundation.wallet.ui.wallets.WalletsModel +import dagger.android.AndroidInjection +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject +import javax.inject.Inject + +class SettingsActivity : BaseActivity(), HasAndroidInjector, SettingsActivityView { + + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + private var authenticationResultSubject: PublishSubject? = null + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + toolbar() + authenticationResultSubject = PublishSubject.create() + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, SettingsFragment()) + .commit() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + TransactionsRouter().open(this, true) + finish() + return true + } + return super.onOptionsItemSelected(item) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == AUTHENTICATION_REQUEST_CODE) + if (resultCode == AuthenticationPromptActivity.RESULT_OK) { + authenticationResultSubject?.onNext(true) + } + } + + override fun androidInjector() = androidInjector + + override fun showWalletsBottomSheet(walletModel: WalletsModel) { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.fade_in_animation, R.anim.fragment_slide_down, + R.anim.fade_in_animation, R.anim.fragment_slide_down) + .replace(R.id.bottom_sheet_fragment_container, + SettingsWalletsFragment.newInstance(walletModel)) + .addToBackStack(SettingsWalletsFragment::class.java.simpleName) + .commit() + } + + override fun navigateToBackup(address: String, popBackStack: Boolean) { + startActivity(WalletBackupActivity.newIntent(this, address)) + if (popBackStack) supportFragmentManager.popBackStack() + } + + override fun hideBottomSheet() = supportFragmentManager.popBackStack() + + override fun showAuthentication() { + val intent = AuthenticationPromptActivity.newIntent(this) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivityForResult(intent, AUTHENTICATION_REQUEST_CODE) + } + + override fun authenticationResult(): Observable = authenticationResultSubject!! + + override fun onDestroy() { + authenticationResultSubject = null + super.onDestroy() + } + + private companion object { + private const val AUTHENTICATION_REQUEST_CODE = 33 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsActivityView.kt b/app/src/main/java/com/asfoundation/wallet/ui/SettingsActivityView.kt new file mode 100644 index 00000000000..1d815ac33ab --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SettingsActivityView.kt @@ -0,0 +1,17 @@ +package com.asfoundation.wallet.ui + +import com.asfoundation.wallet.ui.wallets.WalletsModel +import io.reactivex.Observable + +interface SettingsActivityView { + + fun showWalletsBottomSheet(walletModel: WalletsModel) + + fun navigateToBackup(address: String, popBackStack: Boolean = false) + + fun hideBottomSheet() + + fun showAuthentication() + + fun authenticationResult(): Observable +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsFragment.java b/app/src/main/java/com/asfoundation/wallet/ui/SettingsFragment.java deleted file mode 100644 index 494bec5ce1d..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/SettingsFragment.java +++ /dev/null @@ -1,186 +0,0 @@ -package com.asfoundation.wallet.ui; - -import android.app.AlertDialog; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; -import android.support.annotation.Nullable; -import android.view.View; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; -import com.asfoundation.wallet.router.ManageWalletsRouter; -import com.asfoundation.wallet.router.SendRouter; -import dagger.android.AndroidInjection; -import javax.inject.Inject; - -public class SettingsFragment extends PreferenceFragment - implements SharedPreferences.OnSharedPreferenceChangeListener { - @Inject EthereumNetworkRepositoryType ethereumNetworkRepository; - @Inject FindDefaultWalletInteract findDefaultWalletInteract; - @Inject ManageWalletsRouter manageWalletsRouter; - SendRouter sendRouter = new SendRouter(); - - @Override public void onCreate(Bundle savedInstanceState) { - AndroidInjection.inject(this); - super.onCreate(savedInstanceState); - } - - @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - // Load the preferences from an XML resource - addPreferencesFromResource(R.xml.fragment_settings); - final Preference wallets = findPreference("pref_wallet"); - - wallets.setOnPreferenceClickListener(preference -> { - manageWalletsRouter.open(getActivity(), false); - return false; - }); - - findDefaultWalletInteract.find() - .subscribe(wallet -> { - PreferenceManager.getDefaultSharedPreferences(view.getContext()) - .edit() - .putString("pref_wallet", wallet.address) - .apply(); - wallets.setSummary(wallet.address); - }, t -> { - }); - - final ListPreference listPreference = (ListPreference) findPreference("pref_rpcServer"); - // THIS IS REQUIRED IF YOU DON'T HAVE 'entries' and 'entryValues' in your XML - setRpcServerPreferenceData(listPreference); - listPreference.setOnPreferenceClickListener(preference -> { - setRpcServerPreferenceData(listPreference); - return false; - }); - String versionString = getVersion(); - Preference version = findPreference("pref_version"); - version.setSummary(versionString); - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - preferences.registerOnSharedPreferenceChangeListener(SettingsFragment.this); - - final Preference twitter = findPreference("pref_twitter"); - twitter.setOnPreferenceClickListener(preference -> { - Intent intent; - try { - // get the Twitter app if possible - getActivity().getPackageManager() - .getPackageInfo("com.twitter.android", 0); - intent = - new Intent(Intent.ACTION_VIEW, Uri.parse("twitter://user?user_id=915531221551255552")); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } catch (Exception e) { - // no Twitter app, revert to browser - intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/AppCoinsProject")); - } - startActivity(intent); - return false; - }); - - final Preference facebook = findPreference("pref_facebook"); - facebook.setOnPreferenceClickListener(preference -> { - Intent intent = - new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.facebook.com/AppCoinsOfficial")); - startActivity(intent); - return false; - }); - - final Preference email = findPreference("pref_email"); - email.setOnPreferenceClickListener(preference -> { - - Intent mailto = new Intent(Intent.ACTION_SEND_MULTIPLE); - mailto.setType("message/rfc822"); // use from live device - mailto.putExtra(Intent.EXTRA_EMAIL, new String[] { - "info@appcoins.io" - }); - mailto.putExtra(Intent.EXTRA_SUBJECT, "Android wallet support question"); - mailto.putExtra(Intent.EXTRA_TEXT, "Dear AppCoins support,"); - - startActivity(Intent.createChooser(mailto, "Select email application.")); - return true; - }); - - final Preference credits = findPreference("pref_credits"); - credits.setOnPreferenceClickListener(preference -> { - new AlertDialog.Builder(getActivity()).setPositiveButton(R.string.close, - (dialog, which) -> dialog.dismiss()) - .setMessage(R.string.settings_fragment_credits) - .create() - .show(); - return true; - }); - } - - private void rateThisApp() { - Uri uri = Uri.parse("market://details?id=" + getActivity().getPackageName()); - Intent goToMarket = new Intent(Intent.ACTION_VIEW, uri); - // To count with Play market backstack, After pressing back button, - // to taken back to our application, we need to add following flags to intent. - goToMarket.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - try { - startActivity(goToMarket); - } catch (ActivityNotFoundException e) { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse( - "http://play.google.com/store/apps/details?id=" + getActivity().getPackageName()))); - } - } - - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (key.equals("pref_rpcServer")) { - Preference rpcServerPref = findPreference(key); - // Set summary - String selectedRpcServer = sharedPreferences.getString(key, ""); - rpcServerPref.setSummary(selectedRpcServer); - NetworkInfo[] networks = ethereumNetworkRepository.getAvailableNetworkList(); - for (NetworkInfo networkInfo : networks) { - if (networkInfo.name.equals(selectedRpcServer)) { - ethereumNetworkRepository.setDefaultNetworkInfo(networkInfo); - return; - } - } - } - } - - private void setRpcServerPreferenceData(ListPreference lp) { - NetworkInfo[] networks = ethereumNetworkRepository.getAvailableNetworkList(); - CharSequence[] entries = new CharSequence[networks.length]; - for (int ii = 0; ii < networks.length; ii++) { - entries[ii] = networks[ii].name; - } - - CharSequence[] entryValues = new CharSequence[networks.length]; - for (int ii = 0; ii < networks.length; ii++) { - entryValues[ii] = networks[ii].name; - } - - String currentValue = ethereumNetworkRepository.getDefaultNetwork().name; - - lp.setEntries(entries); - lp.setDefaultValue(currentValue); - lp.setValue(currentValue); - lp.setSummary(currentValue); - lp.setEntryValues(entryValues); - } - - public String getVersion() { - String version = "N/A"; - try { - PackageInfo pInfo = getActivity().getPackageManager() - .getPackageInfo(getActivity().getPackageName(), 0); - version = pInfo.versionName; - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - return version; - } -} - diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/SettingsFragment.kt new file mode 100644 index 00000000000..70b2e51cadc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SettingsFragment.kt @@ -0,0 +1,310 @@ +package com.asfoundation.wallet.ui + +import android.app.AlertDialog +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat +import com.asf.wallet.BuildConfig +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.PageViewAnalytics +import com.asfoundation.wallet.permissions.manage.view.ManagePermissionsActivity +import com.asfoundation.wallet.ui.balance.RestoreWalletActivity +import com.google.android.material.snackbar.Snackbar +import dagger.android.support.AndroidSupportInjection +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.preference_fingerprint.* +import kotlinx.android.synthetic.main.preference_fingerprint_off.* +import javax.inject.Inject + +class SettingsFragment : PreferenceFragmentCompat(), SettingsView { + + @Inject + lateinit var settingsInteract: SettingsInteractor + + @Inject + lateinit var pageViewAnalytics: PageViewAnalytics + + private lateinit var presenter: SettingsPresenter + private lateinit var activityView: SettingsActivityView + private var switchSubject: PublishSubject? = null + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is SettingsActivityView) { + throw IllegalStateException("Settings Fragment must be attached to Settings Activity") + } + activityView = context + } + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidSupportInjection.inject(this) + super.onCreate(savedInstanceState) + switchSubject = PublishSubject.create() + presenter = + SettingsPresenter(this, activityView, Schedulers.io(), AndroidSchedulers.mainThread(), + CompositeDisposable(), settingsInteract) + presenter.setFingerPrintPreference() + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.fragment_settings, rootKey) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present() + } + + override fun onResume() { + super.onResume() + pageViewAnalytics.sendPageViewEvent(javaClass.simpleName) + presenter.onResume() + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + override fun onDestroy() { + switchSubject = null + super.onDestroy() + } + + private fun startBrowserActivity(uri: Uri, newTaskFlag: Boolean) { + try { + val intent = Intent(Intent.ACTION_VIEW, uri) + if (newTaskFlag) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + } catch (exception: ActivityNotFoundException) { + exception.printStackTrace() + showError() + } + } + + private fun openPermissionScreen(): Boolean { + context?.let { + val intent = ManagePermissionsActivity.newIntent(it) + .apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP } + startActivity(intent) + } + return true + } + + override fun showError() { + view?.let { + Snackbar.make(it, R.string.unknown_error, Snackbar.LENGTH_SHORT) + .show() + } + } + + override fun setBackupPreference() { + val backupPreference = findPreference("pref_backup") + backupPreference?.setOnPreferenceClickListener { + presenter.onBackupPreferenceClick() + false + } + } + + override fun setRestorePreference() { + val restorePreference = findPreference("pref_restore") + restorePreference?.setOnPreferenceClickListener { + context?.let { startActivity(RestoreWalletActivity.newIntent(it)) } + false + } + } + + override fun setRedeemCodePreference(walletAddress: String) { + val redeemPreference = findPreference("pref_redeem") + redeemPreference?.setOnPreferenceClickListener { + startBrowserActivity(Uri.parse( + BuildConfig.MY_APPCOINS_BASE_HOST + "redeem?wallet_address=" + walletAddress), + false) + false + } + } + + override fun navigateToIntent(intent: Intent) = startActivity(intent) + + override fun authenticationResult(): Observable { + return activityView.authenticationResult() + } + + override fun toggleFingerprint(enabled: Boolean) { + if (pref_authentication_switch == null && pref_authentication_switch_off == null) { + setFingerprintPreference(enabled) + } + pref_authentication_switch?.isChecked = enabled + pref_authentication_switch_off?.isChecked = enabled + } + + override fun setFingerprintPreference(hasAuthenticationPermission: Boolean) { + val fingerprintPreference = findPreference("pref_fingerprint") + + if (hasAuthenticationPermission) { + fingerprintPreference?.layoutResource = R.layout.preference_fingerprint + } else { + fingerprintPreference?.layoutResource = R.layout.preference_fingerprint_off + } + + fingerprintPreference?.setOnPreferenceChangeListener { _, _ -> + switchSubject?.onNext(Unit) + true + } + } + + override fun updateFingerPrintListener(enabled: Boolean) { + val fingerprintPreference = findPreference("pref_fingerprint") + fingerprintPreference?.setOnPreferenceChangeListener { _, _ -> + if (enabled) switchSubject?.onNext(Unit) + true + } + } + + override fun switchPreferenceChange() = switchSubject!! + + override fun removeFingerprintPreference() { + val fingerPrintPreference = findPreference("pref_fingerprint") + fingerPrintPreference?.isVisible = false + } + + override fun setDisabledFingerPrintPreference() { + val fingerprintPreference = findPreference("pref_fingerprint") + fingerprintPreference?.isChecked = false + fingerprintPreference?.layoutResource = R.layout.preference_fingerprint_off + fingerprintPreference?.setOnPreferenceChangeListener { _, _ -> + true + } + } + + override fun setPermissionPreference() { + val permissionPreference = findPreference("pref_permissions") + permissionPreference?.setOnPreferenceClickListener { openPermissionScreen() } + } + + override fun setSourceCodePreference() { + val sourceCodePreference = findPreference("pref_source_code") + sourceCodePreference?.setOnPreferenceClickListener { + startBrowserActivity(Uri.parse("https://github.com/Aptoide/appcoins-wallet-android"), false) + false + } + } + + override fun setIssueReportPreference() { + val bugReportPreference = findPreference("pref_contact_support") + bugReportPreference?.setOnPreferenceClickListener { + presenter.onBugReportClicked() + false + } + } + + override fun setTwitterPreference() { + val twitterPreference = findPreference("pref_twitter") + twitterPreference?.setOnPreferenceClickListener { + try { + activity?.packageManager?.getPackageInfo("com.twitter.android", 0) + startBrowserActivity(Uri.parse("twitter://user?user_id=915531221551255552"), true) + } catch (e: Exception) { + startBrowserActivity(Uri.parse("https://twitter.com/AppCoinsProject"), false) + } + false + } + } + + override fun setTelegramPreference() { + val telegramPreference = findPreference("pref_telegram") + telegramPreference?.setOnPreferenceClickListener { + startBrowserActivity(Uri.parse("https://t.me/appcoinsofficial"), false) + false + } + } + + override fun setFacebookPreference() { + val facebookPreference = findPreference("pref_facebook") + facebookPreference?.setOnPreferenceClickListener { + startBrowserActivity(Uri.parse("https://www.facebook.com/AppCoinsOfficial"), false) + false + } + } + + override fun setEmailPreference() { + val emailPreference = findPreference("pref_email") + emailPreference?.setOnPreferenceClickListener { + val email = "info@appcoins.io" + val subject = "Android wallet support question" + val body = "Dear AppCoins support," + val emailAppIntent = Intent(Intent.ACTION_SENDTO) + emailAppIntent.data = Uri.parse("mailto:") + emailAppIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) + emailAppIntent.putExtra(Intent.EXTRA_SUBJECT, subject) + emailAppIntent.putExtra(Intent.EXTRA_TEXT, body) + startActivity(Intent.createChooser(emailAppIntent, "Select email application.")) + true + } + } + + override fun setPrivacyPolicyPreference() { + val privacyPolicyPreference = findPreference("pref_privacy_policy") + privacyPolicyPreference?.setOnPreferenceClickListener { + startBrowserActivity(Uri.parse("https://catappult.io/appcoins-wallet/privacy-policy"), + false) + false + } + } + + override fun setTermsConditionsPreference() { + val termsConditionsPreference = findPreference("pref_terms_condition") + termsConditionsPreference?.setOnPreferenceClickListener { + startBrowserActivity(Uri.parse("https://catappult.io/appcoins-wallet/terms-conditions"), + false) + false + } + } + + override fun setCreditsPreference() { + val creditsPreference = findPreference("pref_credits") + creditsPreference?.setOnPreferenceClickListener { + AlertDialog.Builder(activity) + .setPositiveButton(R.string.close + ) { dialog, _ -> dialog.dismiss() } + .setMessage(R.string.settings_fragment_credits) + .create() + .show() + true + } + } + + override fun setVersionPreference() { + val versionString = getVersion() + val versionPreference = findPreference("pref_version") + versionPreference?.summary = getString(R.string.check_updates_settings_subtitle, versionString) + versionPreference?.setOnPreferenceClickListener { + presenter.redirectToStore() + false + } + } + + private fun getVersion(): String? { + var version: String? = "N/A" + try { + activity?.let { + val pInfo = it.packageManager?.getPackageInfo(it.packageName, 0) + version = pInfo?.versionName + } + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + } + return version + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsInteractor.kt b/app/src/main/java/com/asfoundation/wallet/ui/SettingsInteractor.kt new file mode 100644 index 00000000000..fd861c3da45 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SettingsInteractor.kt @@ -0,0 +1,51 @@ +package com.asfoundation.wallet.ui + +import com.asfoundation.wallet.billing.analytics.WalletsAnalytics +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.interact.AutoUpdateInteract +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.ui.wallets.WalletsInteract + +class SettingsInteractor(private val findDefaultWalletInteract: FindDefaultWalletInteract, + private val supportInteractor: SupportInteractor, + private val walletsInteract: WalletsInteract, + private val autoUpdateInteract: AutoUpdateInteract, + private val fingerPrintInteractor: FingerPrintInteractor, + private val walletsEventSender: WalletsEventSender, + private val preferencesRepositoryType: PreferencesRepositoryType) { + + private var fingerPrintAvailability: Int = -1 + + fun findWallet() = findDefaultWalletInteract.find() + .map { it.address } + + fun retrieveWallets() = walletsInteract.retrieveWalletsModel() + + fun sendCreateSuccessEvent() { + walletsEventSender.sendCreateBackupEvent(WalletsAnalytics.ACTION_CREATE, + WalletsAnalytics.CONTEXT_WALLET_SETTINGS, WalletsAnalytics.STATUS_SUCCESS) + } + + fun sendCreateErrorEvent() { + walletsEventSender.sendCreateBackupEvent(WalletsAnalytics.ACTION_CREATE, + WalletsAnalytics.CONTEXT_WALLET_SETTINGS, WalletsAnalytics.STATUS_FAIL) + } + + fun displaySupportScreen() = supportInteractor.displayChatScreen() + + fun retrieveUpdateIntent() = autoUpdateInteract.buildUpdateIntent() + + fun retrieveFingerPrintAvailability(): Int { + fingerPrintAvailability = fingerPrintInteractor.getDeviceCompatibility() + return fingerPrintAvailability + } + + fun retrievePreviousFingerPrintAvailability() = fingerPrintAvailability + + fun changeAuthorizationPermission(value: Boolean) = + preferencesRepositoryType.setAuthenticationPermission(value) + + fun hasAuthenticationPermission() = preferencesRepositoryType.hasAuthenticationPermission() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/SettingsPresenter.kt new file mode 100644 index 00000000000..13abc893b21 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SettingsPresenter.kt @@ -0,0 +1,154 @@ +package com.asfoundation.wallet.ui + +import android.content.Intent +import android.hardware.biometrics.BiometricManager +import com.asfoundation.wallet.ui.wallets.WalletsModel +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable + +class SettingsPresenter(private val view: SettingsView, + private val activityView: SettingsActivityView, + private val networkScheduler: Scheduler, + private val viewScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val settingsInteractor: SettingsInteractor) { + + fun present() { + handleAuthenticationResult() + onFingerPrintPreferenceChange() + } + + fun onResume() { + updateFingerPrintPreference(settingsInteractor.retrievePreviousFingerPrintAvailability()) + setupPreferences() + handleRedeemPreferenceSetup() + } + + private fun setupPreferences() { + view.setPermissionPreference() + view.setSourceCodePreference() + view.setIssueReportPreference() + view.setTwitterPreference() + view.setTelegramPreference() + view.setFacebookPreference() + view.setEmailPreference() + view.setPrivacyPolicyPreference() + view.setTermsConditionsPreference() + view.setCreditsPreference() + view.setVersionPreference() + view.setRestorePreference() + view.setBackupPreference() + } + + fun setFingerPrintPreference() { + when (settingsInteractor.retrieveFingerPrintAvailability()) { + BiometricManager.BIOMETRIC_SUCCESS -> view.setFingerprintPreference( + settingsInteractor.hasAuthenticationPermission()) + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> { + settingsInteractor.changeAuthorizationPermission(false) + view.removeFingerprintPreference() + } + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { + view.toggleFingerprint(false) + settingsInteractor.changeAuthorizationPermission(false) + view.setDisabledFingerPrintPreference() + } + } + } + + private fun updateFingerPrintPreference(previousAvailability: Int) { + val newAvailability = settingsInteractor.retrieveFingerPrintAvailability() + if (previousAvailability != newAvailability) { + when (settingsInteractor.retrieveFingerPrintAvailability()) { + BiometricManager.BIOMETRIC_SUCCESS -> { + if (previousAvailability == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { + view.setFingerprintPreference(settingsInteractor.hasAuthenticationPermission()) + } else { + view.updateFingerPrintListener(true) + } + } + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> { + settingsInteractor.changeAuthorizationPermission(false) + view.removeFingerprintPreference() + } + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { + view.toggleFingerprint(false) + settingsInteractor.changeAuthorizationPermission(false) + if (previousAvailability == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { + view.setDisabledFingerPrintPreference() + } else { + view.updateFingerPrintListener(false) + } + } + } + } + } + + private fun handleAuthenticationResult() { + disposables.add(view.authenticationResult() + .filter { it } + .doOnNext { + settingsInteractor.changeAuthorizationPermission(false) + view.toggleFingerprint(false) + } + .subscribe({}, { it.printStackTrace() })) + } + + fun stop() = disposables.dispose() + + private fun handleRedeemPreferenceSetup() { + disposables.add(settingsInteractor.findWallet() + .doOnSuccess { view.setRedeemCodePreference(it) } + .subscribe({}, { it.printStackTrace() })) + } + + fun onBackupPreferenceClick() { + disposables.add(settingsInteractor.retrieveWallets() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { handleWalletModel(it) } + .subscribe({}, { handleError(it) })) + } + + private fun handleWalletModel(walletModel: WalletsModel) { + when (walletModel.totalWallets) { + 0 -> { + settingsInteractor.sendCreateErrorEvent() + view.showError() + } + 1 -> { + settingsInteractor.sendCreateSuccessEvent() + activityView.navigateToBackup(walletModel.walletsBalance[0].walletAddress) + } + else -> activityView.showWalletsBottomSheet(walletModel) + } + } + + fun onBugReportClicked() = settingsInteractor.displaySupportScreen() + + fun redirectToStore() { + disposables.add( + Single.create { it.onSuccess(settingsInteractor.retrieveUpdateIntent()) } + .doOnSuccess { view.navigateToIntent(it) } + .subscribe({}, { handleError(it) })) + } + + private fun handleError(throwable: Throwable) { + throwable.printStackTrace() + view.showError() + } + + private fun onFingerPrintPreferenceChange() { + disposables.add(view.switchPreferenceChange() + .doOnNext { + if (settingsInteractor.hasAuthenticationPermission()) activityView.showAuthentication() + else { + view.toggleFingerprint(true) + settingsInteractor.changeAuthorizationPermission(true) + } + } + .subscribe({}, { it.printStackTrace() })) + } +} + diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsView.kt b/app/src/main/java/com/asfoundation/wallet/ui/SettingsView.kt new file mode 100644 index 00000000000..e4f0df8f1a1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SettingsView.kt @@ -0,0 +1,54 @@ +package com.asfoundation.wallet.ui + +import android.content.Intent +import io.reactivex.Observable + + +interface SettingsView { + + fun setRedeemCodePreference(walletAddress: String) + + fun showError() + + fun navigateToIntent(intent: Intent) + + fun authenticationResult(): Observable + + fun toggleFingerprint(enabled: Boolean) + + fun setPermissionPreference() + + fun setSourceCodePreference() + + fun setFingerprintPreference(hasAuthenticationPermission: Boolean) + + fun setTwitterPreference() + + fun setIssueReportPreference() + + fun setFacebookPreference() + + fun setTelegramPreference() + + fun setEmailPreference() + + fun setPrivacyPolicyPreference() + + fun setTermsConditionsPreference() + + fun setCreditsPreference() + + fun setVersionPreference() + + fun setRestorePreference() + + fun setBackupPreference() + + fun removeFingerprintPreference() + + fun setDisabledFingerPrintPreference() + + fun switchPreferenceChange(): Observable + + fun updateFingerPrintListener(enabled: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsBottomSheetFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsBottomSheetFragment.kt new file mode 100644 index 00000000000..412d07ddc07 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsBottomSheetFragment.kt @@ -0,0 +1,108 @@ +package com.asfoundation.wallet.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.ui.wallets.WalletBalance +import com.asfoundation.wallet.ui.wallets.WalletsAdapter +import com.asfoundation.wallet.ui.wallets.WalletsModel +import com.asfoundation.wallet.ui.wallets.WalletsViewType +import com.asfoundation.wallet.ui.widget.MarginItemDecoration +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.settings_wallet_bottom_sheet_layout.* +import javax.inject.Inject + +class SettingsWalletsBottomSheetFragment : BasePageViewFragment(), SettingsWalletsBottomSheetView { + + + @Inject + lateinit var currencyFormatter: CurrencyFormatUtils + + @Inject + lateinit var walletsEventSender: WalletsEventSender + private lateinit var presenter: SettingsWalletsBottomSheetPresenter + private lateinit var adapter: WalletsAdapter + private var uiEventListener: PublishSubject? = null + + companion object { + + private const val WALLET_MODEL_KEY = "wallet_model" + + @JvmStatic + fun newInstance(walletsModel: WalletsModel): SettingsWalletsBottomSheetFragment { + return SettingsWalletsBottomSheetFragment().apply { + arguments = Bundle().apply { + putSerializable(WALLET_MODEL_KEY, walletsModel) + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + uiEventListener = PublishSubject.create() + presenter = SettingsWalletsBottomSheetPresenter(this, CompositeDisposable(), walletsEventSender, + walletsModel) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.settings_wallet_bottom_sheet_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present() + } + + override fun setupUi(walletsBalance: List) { + val layoutManager = LinearLayoutManager(context) + layoutManager.orientation = RecyclerView.VERTICAL + adapter = + WalletsAdapter(context!!, walletsBalance, uiEventListener!!, currencyFormatter, + WalletsViewType.SETTINGS) + bottom_sheet_wallets_cards.addItemDecoration(MarginItemDecoration( + resources.getDimension(R.dimen.wallets_card_margin) + .toInt())) + bottom_sheet_wallets_cards.isNestedScrollingEnabled = false + bottom_sheet_wallets_cards.layoutManager = layoutManager + bottom_sheet_wallets_cards.adapter = adapter + val parent = provideParentFragment() + parent?.showBottomSheet() + } + + override fun walletCardClicked() = uiEventListener!! + + override fun navigateToBackup(address: String) { + val parent = provideParentFragment() + parent?.navigateToBackup(address) + } + + private fun provideParentFragment(): SettingsWalletsView? { + if (parentFragment !is SettingsWalletsView) { + return null + } + return parentFragment as SettingsWalletsView + } + + override fun onDestroyView() { + super.onDestroyView() + presenter.stop() + } + + private val walletsModel: WalletsModel by lazy { + if (arguments!!.containsKey(WALLET_MODEL_KEY)) { + arguments!!.getSerializable(WALLET_MODEL_KEY) as WalletsModel + } else { + throw IllegalArgumentException("WalletsModel not available") + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsBottomSheetPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsBottomSheetPresenter.kt new file mode 100644 index 00000000000..71b36b66107 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsBottomSheetPresenter.kt @@ -0,0 +1,35 @@ +package com.asfoundation.wallet.ui + +import com.asfoundation.wallet.billing.analytics.WalletsAnalytics +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.ui.wallets.WalletsModel +import io.reactivex.disposables.CompositeDisposable + +class SettingsWalletsBottomSheetPresenter( + private val view: SettingsWalletsBottomSheetView, + private val disposables: CompositeDisposable, + private val walletsEventSender: WalletsEventSender, + private val walletsModel: WalletsModel) { + + fun present() { + view.setupUi(walletsModel.walletsBalance) + handleWalletCardClick() + } + + private fun handleWalletCardClick() { + disposables.add(view.walletCardClicked() + .doOnNext { + walletsEventSender.sendCreateBackupEvent(WalletsAnalytics.ACTION_CREATE, + WalletsAnalytics.CONTEXT_WALLET_SETTINGS, WalletsAnalytics.STATUS_SUCCESS) + } + .doOnNext { view.navigateToBackup(it) } + .doOnError { + walletsEventSender.sendCreateBackupEvent(WalletsAnalytics.ACTION_CREATE, + WalletsAnalytics.CONTEXT_WALLET_SETTINGS, WalletsAnalytics.STATUS_FAIL) + } + .subscribe({}, { it.printStackTrace() })) + } + + + fun stop() = disposables.clear() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsBottomSheetView.kt b/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsBottomSheetView.kt new file mode 100644 index 00000000000..381f4db05dd --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsBottomSheetView.kt @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.ui + +import com.asfoundation.wallet.ui.wallets.WalletBalance +import io.reactivex.Observable + +interface SettingsWalletsBottomSheetView { + + fun setupUi(walletsBalance: List) + + fun walletCardClicked(): Observable + + fun navigateToBackup(address: String) +} + diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsFragment.kt new file mode 100644 index 00000000000..e5bf21b67a8 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsFragment.kt @@ -0,0 +1,100 @@ +package com.asfoundation.wallet.ui + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.AnimationUtils +import androidx.fragment.app.Fragment +import com.asf.wallet.R +import com.asfoundation.wallet.ui.wallets.WalletsModel +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.fragment_balance.* + +class SettingsWalletsFragment : Fragment(), SettingsWalletsView { + + private lateinit var walletsBottomSheet: BottomSheetBehavior + private lateinit var activityView: SettingsActivityView + private lateinit var presenter: SettingsWalletsPresenter + + companion object { + private const val WALLET_MODEL_KEY = "wallet_model" + + @JvmStatic + fun newInstance(walletsModel: WalletsModel): SettingsWalletsFragment { + return SettingsWalletsFragment().apply { + arguments = Bundle().apply { + putSerializable(WALLET_MODEL_KEY, walletsModel) + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = SettingsWalletsPresenter(this, activityView, CompositeDisposable()) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is SettingsActivityView) { + throw IllegalStateException("Settings Fragment must be attached to Settings Activity") + } + activityView = context + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + childFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.fragment_slide_up, R.anim.fragment_slide_down, + R.anim.fragment_slide_up, R.anim.fragment_slide_down) + .replace(R.id.bottom_sheet_fragment_container, + SettingsWalletsBottomSheetFragment.newInstance(walletsModel)) + .commit() + return inflater.inflate(R.layout.settings_wallets_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + walletsBottomSheet = + BottomSheetBehavior.from(bottom_sheet_fragment_container) + presenter.present() + } + + override fun onDestroyView() { + super.onDestroyView() + faded_background.animation = + AnimationUtils.loadAnimation(context, R.anim.fast_100s_fade_out_animation) + faded_background.visibility = View.GONE + presenter.stop() + } + + override fun showBottomSheet() { + walletsBottomSheet.state = BottomSheetBehavior.STATE_EXPANDED + walletsBottomSheet.isFitToContents = true + walletsBottomSheet.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_DRAGGING) { + walletsBottomSheet.state = BottomSheetBehavior.STATE_EXPANDED + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit + }) + } + + override fun outsideOfBottomSheetClick() = RxView.clicks(faded_background) + + override fun navigateToBackup(address: String) = activityView.navigateToBackup(address, true) + + private val walletsModel: WalletsModel by lazy { + if (arguments!!.containsKey(WALLET_MODEL_KEY)) { + arguments!!.getSerializable(WALLET_MODEL_KEY) as WalletsModel + } else { + throw IllegalArgumentException("WalletsModel not available") + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsPresenter.kt new file mode 100644 index 00000000000..466063093dd --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsPresenter.kt @@ -0,0 +1,20 @@ +package com.asfoundation.wallet.ui + +import io.reactivex.disposables.CompositeDisposable + +class SettingsWalletsPresenter(private val view: SettingsWalletsView, + private val activityView: SettingsActivityView, + private val disposables: CompositeDisposable) { + + fun present() { + handleOutsideOfBottomSheetClick() + } + + private fun handleOutsideOfBottomSheetClick() { + disposables.add(view.outsideOfBottomSheetClick() + .doOnNext { activityView.hideBottomSheet() } + .subscribe({}, { it.printStackTrace() })) + } + + fun stop() = disposables.clear() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsView.kt b/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsView.kt new file mode 100644 index 00000000000..fc2dabdce7d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SettingsWalletsView.kt @@ -0,0 +1,12 @@ +package com.asfoundation.wallet.ui + +import io.reactivex.Observable + +interface SettingsWalletsView { + + fun showBottomSheet() + + fun navigateToBackup(address: String) + + fun outsideOfBottomSheetClick(): Observable +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SplashActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/SplashActivity.java index e56ef4c3618..339bbf13e35 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/SplashActivity.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/SplashActivity.java @@ -1,21 +1,25 @@ package com.asfoundation.wallet.ui; -import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.router.ManageWalletsRouter; +import androidx.annotation.NonNull; +import com.asfoundation.wallet.interact.AutoUpdateInteract; +import com.asfoundation.wallet.repository.PreferencesRepositoryType; +import com.asfoundation.wallet.router.OnboardingRouter; import com.asfoundation.wallet.router.TransactionsRouter; -import com.asfoundation.wallet.viewmodel.SplashViewModel; -import com.asfoundation.wallet.viewmodel.SplashViewModelFactory; import dagger.android.AndroidInjection; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; import javax.inject.Inject; -public class SplashActivity extends BaseActivity { +public class SplashActivity extends BaseActivity implements SplashView { - @Inject SplashViewModelFactory splashViewModelFactory; - SplashViewModel splashViewModel; + private static final int AUTHENTICATION_REQUEST_CODE = 33; + @Inject PreferencesRepositoryType preferencesRepositoryType; + @Inject AutoUpdateInteract autoUpdateInteract; + private SplashPresenter presenter; public static Intent newIntent(Context context) { return new Intent(context, SplashActivity.class); @@ -25,19 +29,57 @@ public static Intent newIntent(Context context) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); - splashViewModel = ViewModelProviders.of(this, splashViewModelFactory) - .get(SplashViewModel.class); - splashViewModel.wallets() - .observe(this, this::onWallets); + presenter = new SplashPresenter(this, preferencesRepositoryType, AndroidSchedulers.mainThread(), + Schedulers.io(), new CompositeDisposable(), autoUpdateInteract); + + presenter.present(savedInstanceState); + } + + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == SplashActivity.AUTHENTICATION_REQUEST_CODE) { + if (resultCode == AuthenticationPromptActivity.RESULT_OK) { + firstScreenNavigation(); + } else { + finish(); + } + } } - private void onWallets(Wallet[] wallets) { - // Start home activity - if (wallets.length == 0) { - new ManageWalletsRouter().open(this, true); + @Override public void navigateToAutoUpdate() { + Intent intent = UpdateRequiredActivity.newIntent(this); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finish(); + } + + @Override public void firstScreenNavigation() { + if (shouldShowOnboarding()) { + new OnboardingRouter().open(this, true); } else { new TransactionsRouter().open(this, true); } finish(); } + + @Override public void showAuthenticationActivity() { + Intent intent = AuthenticationPromptActivity.newIntent(this); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivityForResult(intent, AUTHENTICATION_REQUEST_CODE); + } + + private boolean shouldShowOnboarding() { + return !preferencesRepositoryType.hasCompletedOnboarding(); + } + + @Override protected void onDestroy() { + presenter.stop(); + super.onDestroy(); + } + + @Override public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + presenter.onSaveInstance(outState); + } } + diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SplashPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/SplashPresenter.kt new file mode 100644 index 00000000000..2cec504562d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SplashPresenter.kt @@ -0,0 +1,53 @@ +package com.asfoundation.wallet.ui + +import android.os.Bundle +import com.asfoundation.wallet.interact.AutoUpdateInteract +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + + +class SplashPresenter( + private val view: SplashView, + private val preferencesRepositoryType: PreferencesRepositoryType, + private val viewScheduler: Scheduler, + private val ioScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val autoUpdateInteract: AutoUpdateInteract) { + + private var hasStartedAuth = false + + fun present(savedInstanceState: Bundle?) { + savedInstanceState?.let { hasStartedAuth = it.getBoolean(HAS_STARTED_AUTH) } + if (!hasStartedAuth) handleNavigation() + } + + private fun handleNavigation() { + disposables.add(autoUpdateInteract.getAutoUpdateModel(true) + .subscribeOn(ioScheduler) + .observeOn(viewScheduler) + .doOnSuccess { (updateVersionCode, updateMinSdk, blackList) -> + if (autoUpdateInteract.isHardUpdateRequired(blackList, updateVersionCode, updateMinSdk)) { + view.navigateToAutoUpdate() + } else { + if (preferencesRepositoryType.hasAuthenticationPermission()) { + view.showAuthenticationActivity() + hasStartedAuth = true + } else { + view.firstScreenNavigation() + } + } + } + .subscribe({}, { it.printStackTrace() })) + } + + fun stop() = disposables.clear() + + fun onSaveInstance(outState: Bundle) { + outState.putBoolean(HAS_STARTED_AUTH, hasStartedAuth) + } + + private companion object { + private const val HAS_STARTED_AUTH = "started_auth" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/SplashView.kt b/app/src/main/java/com/asfoundation/wallet/ui/SplashView.kt new file mode 100644 index 00000000000..edcca45a3c8 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/SplashView.kt @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.ui + +interface SplashView { + + fun navigateToAutoUpdate() + fun firstScreenNavigation() + fun showAuthenticationActivity() + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/TokenChangeCollectionActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/TokenChangeCollectionActivity.java deleted file mode 100644 index 55ae8481d49..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/TokenChangeCollectionActivity.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.asfoundation.wallet.ui; - -import android.arch.lifecycle.ViewModelProviders; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.ErrorEnvelope; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.ui.widget.adapter.ChangeTokenCollectionAdapter; -import com.asfoundation.wallet.viewmodel.TokenChangeCollectionViewModel; -import com.asfoundation.wallet.viewmodel.TokenChangeCollectionViewModelFactory; -import com.asfoundation.wallet.widget.SystemView; -import dagger.android.AndroidInjection; -import javax.inject.Inject; - -import static com.asfoundation.wallet.C.ErrorCode.EMPTY_COLLECTION; -import static com.asfoundation.wallet.C.Key.WALLET; - -public class TokenChangeCollectionActivity extends BaseActivity implements View.OnClickListener { - - @Inject protected TokenChangeCollectionViewModelFactory viewModelFactory; - private TokenChangeCollectionViewModel viewModel; - - private ChangeTokenCollectionAdapter adapter; - private SystemView systemView; - - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { - - AndroidInjection.inject(this); - - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_tokens); - - toolbar(); - - adapter = new ChangeTokenCollectionAdapter(this::onTokenClick, this::onTokenDeleteClick); - RecyclerView list = findViewById(R.id.list); - systemView = findViewById(R.id.system_view); - SwipeRefreshLayout refreshLayout = findViewById(R.id.refresh_layout); - - list.setLayoutManager(new LinearLayoutManager(this)); - list.setAdapter(adapter); - - systemView.attachRecyclerView(list); - systemView.attachSwipeRefreshLayout(refreshLayout); - - viewModel = ViewModelProviders.of(this, viewModelFactory) - .get(TokenChangeCollectionViewModel.class); - - viewModel.progress() - .observe(this, systemView::showProgress); - viewModel.error() - .observe(this, this::onError); - viewModel.tokens() - .observe(this, this::onTokens); - viewModel.wallet() - .setValue(getIntent().getParcelableExtra(WALLET)); - - refreshLayout.setOnRefreshListener(viewModel::fetchTokens); - } - - private void onTokenClick(View view, Token token) { - viewModel.setEnabled(token); - } - - private void onTokenDeleteClick(View view, Token token) { - viewModel.deleteToken(token); - } - - @Override protected void onResume() { - super.onResume(); - - viewModel.prepare(); - } - - private void onTokens(Token[] tokens) { - adapter.setTokens(tokens); - } - - private void onError(ErrorEnvelope errorEnvelope) { - if (errorEnvelope.code == EMPTY_COLLECTION) { - systemView.showEmpty(getString(R.string.no_tokens)); - } else { - systemView.showError(getString(R.string.error_fail_load_tokens), this); - } - } - - @Override public void onClick(View view) { - switch (view.getId()) { - case R.id.try_again: { - viewModel.fetchTokens(); - } - break; - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/TokenValue.kt b/app/src/main/java/com/asfoundation/wallet/ui/TokenValue.kt new file mode 100644 index 00000000000..20a36927ba9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/TokenValue.kt @@ -0,0 +1,5 @@ +package com.asfoundation.wallet.ui + +import java.math.BigDecimal + +data class TokenValue(val amount: BigDecimal, val currency: String, val symbol: String = "") diff --git a/app/src/main/java/com/asfoundation/wallet/ui/TokensActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/TokensActivity.java deleted file mode 100644 index 652673d14ed..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/TokensActivity.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.asfoundation.wallet.ui; - -import android.arch.lifecycle.ViewModelProviders; -import android.content.Context; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.ErrorEnvelope; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.ui.widget.adapter.TokensAdapter; -import com.asfoundation.wallet.viewmodel.TokensViewModel; -import com.asfoundation.wallet.viewmodel.TokensViewModelFactory; -import com.asfoundation.wallet.widget.SystemView; -import dagger.android.AndroidInjection; -import java.math.BigDecimal; -import javax.inject.Inject; - -import static com.asfoundation.wallet.C.ErrorCode.EMPTY_COLLECTION; -import static com.asfoundation.wallet.C.Key.WALLET; - -public class TokensActivity extends BaseActivity implements View.OnClickListener { - @Inject TokensViewModelFactory transactionsViewModelFactory; - private TokensViewModel viewModel; - - private SystemView systemView; - private TokensAdapter adapter; - - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { - AndroidInjection.inject(this); - - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_tokens); - - toolbar(); - - adapter = new TokensAdapter(this::onTokenClick); - SwipeRefreshLayout refreshLayout = findViewById(R.id.refresh_layout); - systemView = findViewById(R.id.system_view); - - RecyclerView list = findViewById(R.id.list); - - list.setLayoutManager(new LinearLayoutManager(this)); - list.setAdapter(adapter); - - systemView.attachRecyclerView(list); - systemView.attachSwipeRefreshLayout(refreshLayout); - - viewModel = ViewModelProviders.of(this, transactionsViewModelFactory) - .get(TokensViewModel.class); - viewModel.progress() - .observe(this, systemView::showProgress); - viewModel.error() - .observe(this, this::onError); - viewModel.tokens() - .observe(this, this::onTokens); - viewModel.total() - .observe(this, this::onTotal); - viewModel.wallet() - .observe(this, this::onWallet); - - refreshLayout.setOnRefreshListener(viewModel::fetchTokens); - } - - private void onTotal(BigDecimal totalInCurrency) { - adapter.setTotal(totalInCurrency); - } - - @Override public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_add, menu); - getMenuInflater().inflate(R.menu.menu_edit, menu); - return super.onCreateOptionsMenu(menu); - } - - @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_add: { - viewModel.showAddToken(this); - } - break; - case R.id.action_edit: { - viewModel.showEditTokens(this); - } - break; - case android.R.id.home: { - adapter.clear(); - viewModel.showTransactions(this); - } - } - return super.onOptionsItemSelected(item); - } - - @Override public void onBackPressed() { - viewModel.showTransactions(this); - } - - @Override protected void onResume() { - super.onResume(); - - viewModel.wallet() - .postValue(getIntent().getParcelableExtra(WALLET)); - } - - private void onTokenClick(View view, Token token) { - Context context = view.getContext(); - viewModel.showSendToken(context, token.tokenInfo.address, token.tokenInfo.symbol, - token.tokenInfo.decimals); - } - - private void onWallet(Wallet wallet) { - viewModel.fetchTokens(); - } - - private void onTokens(Token[] tokens) { - adapter.setTokens(tokens); - } - - private void onError(ErrorEnvelope errorEnvelope) { - if (errorEnvelope.code == EMPTY_COLLECTION) { - systemView.showEmpty(getString(R.string.no_tokens)); - } - } - - @Override public void onClick(View view) { - switch (view.getId()) { - case R.id.try_again: { - viewModel.fetchTokens(); - } - break; - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/TransactionDetailActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/TransactionDetailActivity.java deleted file mode 100644 index 1f3ab285b21..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/TransactionDetailActivity.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.asfoundation.wallet.ui; - -import android.arch.lifecycle.ViewModelProviders; -import android.os.Bundle; -import android.support.annotation.ColorRes; -import android.support.annotation.DrawableRes; -import android.support.annotation.Nullable; -import android.support.annotation.StringRes; -import android.support.v7.widget.RecyclerView; -import android.text.format.DateFormat; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.transactions.Operation; -import com.asfoundation.wallet.transactions.Transaction; -import com.asfoundation.wallet.ui.toolbar.ToolbarArcBackground; -import com.asfoundation.wallet.ui.widget.adapter.TransactionsDetailsAdapter; -import com.asfoundation.wallet.util.BalanceUtils; -import com.asfoundation.wallet.viewmodel.TransactionDetailViewModel; -import com.asfoundation.wallet.viewmodel.TransactionDetailViewModelFactory; -import com.asfoundation.wallet.widget.CircleTransformation; -import com.squareup.picasso.Picasso; -import dagger.android.AndroidInjection; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.Calendar; -import java.util.Locale; -import javax.inject.Inject; - -import static com.asfoundation.wallet.C.Key.TRANSACTION; - -public class TransactionDetailActivity extends BaseActivity { - - @Inject TransactionDetailViewModelFactory transactionDetailViewModelFactory; - private TransactionDetailViewModel viewModel; - - private Transaction transaction; - private TextView amount; - private TransactionsDetailsAdapter adapter; - - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - AndroidInjection.inject(this); - - setContentView(R.layout.activity_transaction_detail); - - transaction = getIntent().getParcelableExtra(TRANSACTION); - if (transaction == null) { - finish(); - return; - } - toolbar(); - - ((ToolbarArcBackground) findViewById(R.id.toolbar_background_arc)).setScale(1f); - - amount = findViewById(R.id.amount); - adapter = new TransactionsDetailsAdapter(this::onMoreClicked); - RecyclerView list = findViewById(R.id.details_list); - list.setAdapter(adapter); - - viewModel = ViewModelProviders.of(this, transactionDetailViewModelFactory) - .get(TransactionDetailViewModel.class); - viewModel.defaultNetwork() - .observe(this, this::onDefaultNetwork); - viewModel.defaultWallet() - .observe(this, this::onDefaultWallet); - } - - private void onDefaultWallet(Wallet wallet) { - adapter.setDefaultWallet(wallet); - adapter.addOperations(transaction.getOperations()); - - boolean isSent = transaction.getFrom() - .toLowerCase() - .equals(wallet.address); - - long decimals = 18; - NetworkInfo networkInfo = viewModel.defaultNetwork() - .getValue(); - - String rawValue = transaction.getValue(); - if (!rawValue.equals("0")) { - rawValue = (isSent ? "-" : "+") + getScaledValue(rawValue, decimals); - } - - String symbol = - transaction.getCurrency() == null ? (networkInfo == null ? "" : networkInfo.symbol) - : transaction.getCurrency(); - - String icon = null; - String id = transaction.getTransactionId(); - String description = null; - if (transaction.getDetails() != null) { - icon = transaction.getDetails() - .getIcon(); - id = transaction.getDetails() - .getSourceName(); - description = transaction.getDetails() - .getDescription(); - } - - @StringRes int typeStr = R.string.transaction_type_standard; - @DrawableRes int typeIcon = R.drawable.ic_transaction_peer; - - switch (transaction.getType()) { - case ADS: - typeStr = R.string.transaction_type_poa; - typeIcon = R.drawable.ic_transaction_poa; - break; - case IAB: - typeStr = R.string.transaction_type_iab; - typeIcon = R.drawable.ic_transaction_iab; - break; - } - - @StringRes int statusStr = R.string.transaction_status_success; - @ColorRes int statusColor = R.color.green; - - switch (transaction.getStatus()) { - case FAILED: - statusStr = R.string.transaction_status_failed; - statusColor = R.color.red; - break; - case PENDING: - statusStr = R.string.transaction_status_pending; - statusColor = R.color.orange; - break; - } - - setUIContent(transaction.getTimeStamp(), rawValue, symbol, icon, id, description, typeStr, - typeIcon, statusStr, statusColor); - } - - private void onDefaultNetwork(NetworkInfo networkInfo) { - adapter.setDefaultNetwork(networkInfo); - } - - private String getScaledValue(String valueStr, long decimals) { - // Perform decimal conversion - BigDecimal value = new BigDecimal(valueStr); - value = value.divide(new BigDecimal(Math.pow(10, decimals))); - int scale = 3 - value.precision() + value.scale(); - return value.setScale(scale, RoundingMode.HALF_UP) - .stripTrailingZeros() - .toPlainString(); - } - - private String getDate(long timeStampInSec) { - Calendar cal = Calendar.getInstance(Locale.ENGLISH); - cal.setTimeInMillis(timeStampInSec * 1000); - return DateFormat.format("dd MMM yyyy hh:mm a", cal.getTime()) - .toString(); - } - - private void onMoreClicked(View view, Operation operation) { - viewModel.showMoreDetails(view.getContext(), operation); - } - - private void setUIContent(long timeStamp, String value, String symbol, String icon, String id, - String description, int typeStr, int typeIcon, int statusStr, int statusColor) { - ((TextView) findViewById(R.id.transaction_timestamp)).setText(getDate(timeStamp)); - findViewById(R.id.transaction_timestamp).setVisibility(View.VISIBLE); - - int smallTitleSize = (int) getResources().getDimension(R.dimen.small_text); - int color = getResources().getColor(R.color.gray_alpha_8a); - - amount.setText(BalanceUtils.formatBalance(value, symbol, smallTitleSize, color)); - - if (icon != null) { - Picasso.with(this) - .load("file:" + icon) - .transform(new CircleTransformation()) - .fit() - .into((ImageView) findViewById(R.id.img)); - } else { - ((ImageView) findViewById(R.id.img)).setImageResource(typeIcon); - } - - ((TextView) findViewById(R.id.app_id)).setText(id); - if (description != null) { - ((TextView) findViewById(R.id.item_id)).setText(description); - findViewById(R.id.item_id).setVisibility(View.VISIBLE); - } - ((TextView) findViewById(R.id.category_name)).setText(typeStr); - ((ImageView) findViewById(R.id.category_icon)).setImageResource(typeIcon); - - ((TextView) findViewById(R.id.status)).setText(statusStr); - ((TextView) findViewById(R.id.status)).setTextColor(getResources().getColor(statusColor)); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/TransactionsActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/TransactionsActivity.java index afa74b19f88..5e8a1f2381d 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/TransactionsActivity.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/TransactionsActivity.java @@ -1,62 +1,94 @@ package com.asfoundation.wallet.ui; import android.app.AlertDialog; -import android.app.Dialog; -import android.arch.lifecycle.ViewModelProviders; +import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; -import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.AppBarLayout; -import android.support.design.widget.BottomSheetBehavior; -import android.support.design.widget.BottomSheetDialog; -import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.SpannableString; - +import android.text.Html; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.Toast; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.ShareCompat; +import androidx.core.content.res.ResourcesCompat; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import com.airbnb.lottie.LottieAnimationView; import com.asf.wallet.R; +import com.asfoundation.wallet.entity.Balance; import com.asfoundation.wallet.entity.ErrorEnvelope; +import com.asfoundation.wallet.entity.GlobalBalance; import com.asfoundation.wallet.entity.NetworkInfo; import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.interact.AddTokenInteract; -import com.asfoundation.wallet.poa.TransactionFactory; +import com.asfoundation.wallet.referrals.CardNotification; +import com.asfoundation.wallet.repository.PreferencesRepositoryType; import com.asfoundation.wallet.transactions.Transaction; +import com.asfoundation.wallet.ui.appcoins.applications.AppcoinsApplication; import com.asfoundation.wallet.ui.toolbar.ToolbarArcBackground; import com.asfoundation.wallet.ui.widget.adapter.TransactionsAdapter; -import com.asfoundation.wallet.util.BalanceUtils; +import com.asfoundation.wallet.ui.widget.entity.TransactionsModel; +import com.asfoundation.wallet.ui.widget.holder.ApplicationClickAction; +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction; +import com.asfoundation.wallet.util.CurrencyFormatUtils; import com.asfoundation.wallet.util.RootUtil; +import com.asfoundation.wallet.util.WalletCurrency; import com.asfoundation.wallet.viewmodel.BaseNavigationActivity; import com.asfoundation.wallet.viewmodel.TransactionsViewModel; import com.asfoundation.wallet.viewmodel.TransactionsViewModelFactory; -import com.asfoundation.wallet.widget.DepositView; import com.asfoundation.wallet.widget.EmptyTransactionsView; import com.asfoundation.wallet.widget.SystemView; +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.bottomnavigation.BottomNavigationItemView; +import com.google.android.material.bottomnavigation.BottomNavigationMenuView; +import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; import dagger.android.AndroidInjection; -import java.util.List; -import java.util.Map; +import io.intercom.android.sdk.Intercom; +import io.reactivex.Observable; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.subjects.PublishSubject; import javax.inject.Inject; -import static com.asfoundation.wallet.C.ETHEREUM_NETWORK_NAME; import static com.asfoundation.wallet.C.ErrorCode.EMPTY_COLLECTION; +import static com.asfoundation.wallet.support.SupportNotificationProperties.SUPPORT_NOTIFICATION_CLICK; public class TransactionsActivity extends BaseNavigationActivity implements View.OnClickListener { - public static final String AIRDROP_MORE_INFO_URL = "https://appstorefoundation.org/asf-wallet"; + private static String maxBonusEmptyScreen; @Inject TransactionsViewModelFactory transactionsViewModelFactory; - @Inject AddTokenInteract addTokenInteract; - @Inject TransactionFactory transactionFactory; + @Inject PreferencesRepositoryType preferencesRepositoryType; + @Inject CurrencyFormatUtils formatter; private TransactionsViewModel viewModel; private SystemView systemView; private TransactionsAdapter adapter; - private Dialog dialog; private EmptyTransactionsView emptyView; + private RecyclerView list; + private TextView subtitleView; + private LottieAnimationView balanceSkeleton; + private PublishSubject emptyTransactionsSubject; + private CompositeDisposable disposables; + private View emptyClickableView; + private MenuItem supportActionView; + private View badge; + private int paddingDp; + private boolean showScroll = false; + + public static Intent newIntent(Context context) { + return new Intent(context, TransactionsActivity.class); + } + + public static Intent newIntent(Context context, boolean supportNotificationClicked) { + Intent intent = new Intent(context, TransactionsActivity.class); + intent.putExtra(SUPPORT_NOTIFICATION_CLICK, supportNotificationClicked); + return intent; + } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { AndroidInjection.inject(this); @@ -67,25 +99,53 @@ public class TransactionsActivity extends BaseNavigationActivity implements View toolbar(); enableDisplayHomeAsUp(); - ((AppBarLayout) findViewById(R.id.app_bar)).addOnOffsetChangedListener( - (appBarLayout, verticalOffset) -> { - float percentage = - ((float) Math.abs(verticalOffset) / appBarLayout.getTotalScrollRange()); - findViewById(R.id.toolbar_layout_logo).setAlpha(1 - (percentage * 1.20f)); - ((ToolbarArcBackground) findViewById(R.id.toolbar_background_arc)).setScale(percentage); - }); + disposables = new CompositeDisposable(); + + balanceSkeleton = findViewById(R.id.balance_skeleton); + balanceSkeleton.setVisibility(View.VISIBLE); + emptyClickableView = findViewById(R.id.empty_clickable_view); + emptyClickableView.setVisibility(View.VISIBLE); + balanceSkeleton.playAnimation(); + subtitleView = findViewById(R.id.toolbar_subtitle); + AppBarLayout appBar = findViewById(R.id.app_bar); + appBar.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + float percentage = ((float) Math.abs(verticalOffset) / appBarLayout.getTotalScrollRange()); + float alpha = 1 - (percentage * 1.20f); + findViewById(R.id.toolbar_layout_logo).setAlpha(alpha); + subtitleView.setAlpha(alpha); + balanceSkeleton.setAlpha(alpha); + ((ToolbarArcBackground) findViewById(R.id.toolbar_background_arc)).setScale(percentage); + + if (percentage == 0) { + ((ExtendedFloatingActionButton) findViewById(R.id.top_up_btn)).extend(); + } else { + ((ExtendedFloatingActionButton) findViewById(R.id.top_up_btn)).shrink(); + } + }); - setCollapsingTitle(new SpannableString(getString(R.string.unknown_balance_with_symbol))); + setCollapsingTitle(" "); initBottomNavigation(); disableDisplayHomeAsUp(); - - adapter = new TransactionsAdapter(this::onTransactionClick); + prepareNotificationIcon(); + emptyTransactionsSubject = PublishSubject.create(); + paddingDp = (int) (80 * getResources().getDisplayMetrics().density); + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); + adapter = new TransactionsAdapter(this::onTransactionClick, this::onApplicationClick, + this::onNotificationClick, formatter); + + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override public void onItemRangeInserted(int positionStart, int itemCount) { + if (showScroll) { + linearLayoutManager.smoothScrollToPosition(list, null, 0); + showScroll = false; + } + } + }); SwipeRefreshLayout refreshLayout = findViewById(R.id.refresh_layout); systemView = findViewById(R.id.system_view); - - RecyclerView list = findViewById(R.id.list); - list.setLayoutManager(new LinearLayoutManager(this)); + list = findViewById(R.id.list); list.setAdapter(adapter); + list.setLayoutManager(linearLayoutManager); systemView.attachRecyclerView(list); systemView.attachSwipeRefreshLayout(refreshLayout); @@ -94,73 +154,159 @@ public class TransactionsActivity extends BaseNavigationActivity implements View .get(TransactionsViewModel.class); viewModel.progress() .observe(this, systemView::showProgress); + viewModel.onFetchTransactionsError() + .observe(this, this::onFetchTransactionsError); viewModel.error() .observe(this, this::onError); viewModel.defaultNetwork() .observe(this, this::onDefaultNetwork); - viewModel.defaultWalletBalance() + viewModel.getDefaultWalletBalance() .observe(this, this::onBalanceChanged); viewModel.defaultWallet() .observe(this, this::onDefaultWallet); - viewModel.transactions() - .observe(this, this::onTransactions); + viewModel.transactionsModel() + .observe(this, this::onTransactionsModel); + viewModel.dismissNotification() + .observe(this, this::dismissNotification); + viewModel.gamificationMaxBonus() + .observe(this, this::onGamificationMaxBonus); + viewModel.shouldShowPromotionsNotification() + .observe(this, this::onPromotionsNotification); + viewModel.getUnreadMessages() + .observe(this, this::updateSupportIcon); + viewModel.shareApp() + .observe(this, this::shareApp); refreshLayout.setOnRefreshListener(() -> viewModel.fetchTransactions(true)); + handlePromotionsOverlayVisibility(); + + if (savedInstanceState == null) { + boolean supportNotificationClick = + getIntent().getBooleanExtra(SUPPORT_NOTIFICATION_CLICK, false); + if (supportNotificationClick) { + overridePendingTransition(0, 0); + viewModel.showSupportScreen(true); + } + } } @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_settings: { - viewModel.showSettings(this); - } - break; - case R.id.action_deposit: { - openExchangeDialog(); - } - break; + if (item.getItemId() == R.id.action_settings) { + viewModel.showSettings(this); } return super.onOptionsItemSelected(item); } - private void onBalanceChanged(Map balance) { - if (!balance.isEmpty()) { - Map.Entry entry = balance.entrySet().iterator().next(); - String currency = entry.getKey(); - String value = entry.getValue(); - int smallTitleSize = (int) getResources().getDimension(R.dimen.title_small_text); - int color = getResources().getColor(R.color.appbar_subtitle_color); - setCollapsingTitle(BalanceUtils.formatBalance(value, currency, smallTitleSize, color)); + private void shareApp(String url) { + if (url != null) { + viewModel.clearShareApp(); + ShareCompat.IntentBuilder.from(this) + .setText(url) + .setType("text/plain") + .setChooserTitle(R.string.share_via) + .startChooser(); + } + } + + private void handlePromotionsOverlayVisibility() { + if (!preferencesRepositoryType.isFirstTimeOnTransactionActivity()) { + showPromotionsOverlay(); + preferencesRepositoryType.setFirstTimeOnTransactionActivity(); } } + private void prepareNotificationIcon() { + BottomNavigationMenuView bottomNavigationMenuView = + (BottomNavigationMenuView) ((BottomNavigationView) findViewById( + R.id.bottom_navigation)).getChildAt(0); + int promotionsIconIndex = 0; + View promotionsIcon = bottomNavigationMenuView.getChildAt(promotionsIconIndex); + BottomNavigationItemView itemView = (BottomNavigationItemView) promotionsIcon; + badge = LayoutInflater.from(this) + .inflate(R.layout.notification_badge, bottomNavigationMenuView, false); + badge.setVisibility(View.INVISIBLE); + itemView.addView(badge); + } + + private void onPromotionsNotification(boolean shouldShow) { + if (shouldShow) { + badge.setVisibility(View.VISIBLE); + } else { + badge.setVisibility(View.INVISIBLE); + } + } + + private void updateSupportIcon(boolean hasMessages) { + if (supportActionView == null) { + return; + } + LottieAnimationView animation = findViewById(R.id.intercom_animation); + + if (hasMessages && !animation.isAnimating()) { + animation.playAnimation(); + } else { + animation.cancelAnimation(); + animation.setProgress(0); + } + + animation.setOnClickListener(v -> viewModel.showSupportScreen(false)); + } + + private void onFetchTransactionsError(Double maxBonus) { + if (emptyView == null) { + emptyView = + new EmptyTransactionsView(this, String.valueOf(maxBonus), emptyTransactionsSubject, this, + disposables); + systemView.showEmpty(emptyView); + } + } + + private void onApplicationClick(AppcoinsApplication appcoinsApplication, + ApplicationClickAction applicationClickAction) { + viewModel.onAppClick(appcoinsApplication, applicationClickAction, this); + } + private void onTransactionClick(View view, Transaction transaction) { viewModel.showDetails(view.getContext(), transaction); } + private void onNotificationClick(CardNotification cardNotification, + CardNotificationAction cardNotificationAction) { + viewModel.onNotificationClick(cardNotification, cardNotificationAction, this); + } + @Override protected void onPause() { super.onPause(); - - if (dialog != null && dialog.isShowing()) { - dialog.dismiss(); - } viewModel.pause(); + disposables.dispose(); } @Override protected void onResume() { super.onResume(); - setCollapsingTitle(new SpannableString(getString(R.string.unknown_balance_without_symbol))); - adapter.clear(); - viewModel.prepare(); - checkRoot(); + boolean supportNotificationClick = + getIntent().getBooleanExtra(SUPPORT_NOTIFICATION_CLICK, false); + if (!supportNotificationClick) { + emptyView = null; + if (disposables.isDisposed()) { + disposables = new CompositeDisposable(); + } + adapter.clear(); + list.setVisibility(View.GONE); + viewModel.prepare(); + viewModel.updateConversationCount(); + viewModel.handleUnreadConversationCount(); + checkRoot(); + Intercom.client() + .handlePushMessage(); + } else { + finish(); + } + sendPageViewEvent(); } @Override public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_settings, menu); - - NetworkInfo networkInfo = viewModel.defaultNetwork() - .getValue(); - if (networkInfo != null && networkInfo.name.equals(ETHEREUM_NETWORK_NAME)) { - getMenuInflater().inflate(R.menu.menu_deposit, menu); - } + getMenuInflater().inflate(R.menu.menu_transactions_activity, menu); + supportActionView = menu.findItem(R.id.action_support); + viewModel.handleUnreadConversationCount(); return super.onCreateOptionsMenu(menu); } @@ -170,27 +316,28 @@ private void onTransactionClick(View view, Transaction transaction) { viewModel.fetchTransactions(true); break; } - case R.id.action_air_drop: { - viewModel.showAirDrop(this); + case R.id.top_up_btn: { + viewModel.showTopUp(this); break; } - case R.id.action_learn_more: - openLearnMore(); + case R.id.empty_clickable_view: { + viewModel.showTokens(this); break; + } } } - private void openLearnMore() { - viewModel.onLearnMoreClick(this, Uri.parse(AIRDROP_MORE_INFO_URL)); - } - @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { switch (item.getItemId()) { + case R.id.action_promotions: { + navigateToPromotions(false); + return true; + } case R.id.action_my_address: { viewModel.showMyAddress(this); return true; } - case R.id.action_my_tokens: { + case R.id.action_balance: { viewModel.showTokens(this); return true; } @@ -202,9 +349,30 @@ private void openLearnMore() { return false; } - private void onTransactions(List transaction) { - adapter.addTransactions(transaction); - invalidateOptionsMenu(); + private void onTransactionsModel(TransactionsModel transactionsModel) { + adapter.addItems(transactionsModel); + showList(); + } + + private void showList() { + if (adapter.getTransactionsCount() > 0) { + systemView.setVisibility(View.GONE); + if (list.getPaddingBottom() != paddingDp) { + //Adds padding when there's transactions + list.setPadding(0, 0, 0, paddingDp); + } + list.setVisibility(View.VISIBLE); + } else if (adapter.getNotificationsCount() > 0) { + systemView.setVisibility(View.VISIBLE); + if (list.getPaddingBottom() != 0) { + //Removes padding if the there's no transactions + list.setPadding(0, 0, 0, 0); + } + list.setVisibility(View.VISIBLE); + } else { + systemView.setVisibility(View.VISIBLE); + list.setVisibility(View.GONE); + } } private void onDefaultWallet(Wallet wallet) { @@ -213,51 +381,132 @@ private void onDefaultWallet(Wallet wallet) { private void onDefaultNetwork(NetworkInfo networkInfo) { adapter.setDefaultNetwork(networkInfo); - setBottomMenu(R.menu.menu_main_network); } private void onError(ErrorEnvelope errorEnvelope) { if ((errorEnvelope.code == EMPTY_COLLECTION || adapter.getItemCount() == 0)) { if (emptyView == null) { - emptyView = new EmptyTransactionsView(this, this); + emptyView = new EmptyTransactionsView(this, String.valueOf(maxBonusEmptyScreen), + emptyTransactionsSubject, this, disposables); + systemView.showEmpty(emptyView); } - systemView.showEmpty(emptyView); } } + private void onGamificationMaxBonus(double bonus) { + maxBonusEmptyScreen = Double.toString(bonus); + } + private void checkRoot() { SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); if (RootUtil.isDeviceRooted() && pref.getBoolean("should_show_root_warning", true)) { pref.edit() .putBoolean("should_show_root_warning", false) .apply(); - new AlertDialog.Builder(this).setTitle(R.string.root_title) + AlertDialog alertDialog = new AlertDialog.Builder(this).setTitle(R.string.root_title) .setMessage(R.string.root_body) .setNegativeButton(R.string.ok, (dialog, which) -> { }) .show(); + alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) + .setBackgroundColor(ResourcesCompat.getColor(getResources(), R.color.transparent, null)); + alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) + .setTextColor(ResourcesCompat.getColor(getResources(), R.color.text_button_color, null)); } } - private void openExchangeDialog() { - Wallet wallet = viewModel.defaultWallet() - .getValue(); - if (wallet == null) { - Toast.makeText(this, getString(R.string.error_wallet_not_selected), Toast.LENGTH_SHORT) - .show(); - } else { - BottomSheetDialog dialog = new BottomSheetDialog(this); - DepositView view = new DepositView(this, wallet); - view.setOnDepositClickListener(this::onDepositClick); - dialog.setContentView(view); - BottomSheetBehavior behavior = BottomSheetBehavior.from((View) view.getParent()); - dialog.setOnShowListener(d -> behavior.setPeekHeight(view.getHeight())); - dialog.show(); - this.dialog = dialog; + @Override protected void onDestroy() { + subtitleView = null; + emptyClickableView = null; + balanceSkeleton.removeAllAnimatorListeners(); + balanceSkeleton.removeAllUpdateListeners(); + balanceSkeleton.removeAllLottieOnCompositionLoadedListener(); + emptyTransactionsSubject = null; + disposables.dispose(); + super.onDestroy(); + } + + private void onBalanceChanged(GlobalBalance globalBalance) { + if (globalBalance.getFiatValue() + .length() > 0 && !globalBalance.getFiatSymbol() + .isEmpty()) { + balanceSkeleton.setVisibility(View.GONE); + setCollapsingTitle(globalBalance.getFiatSymbol() + globalBalance.getFiatValue()); + setSubtitle(globalBalance); + } + } + + private void setSubtitle(GlobalBalance globalBalance) { + String subtitle = + buildCurrencyString(globalBalance.getAppcoinsBalance(), globalBalance.getCreditsBalance(), + globalBalance.getEtherBalance(), globalBalance.getShowAppcoins(), + globalBalance.getShowCredits(), globalBalance.getShowEthereum()); + subtitleView.setText(Html.fromHtml(subtitle)); + } + + private String buildCurrencyString(Balance appcoinsBalance, Balance creditsBalance, + Balance ethereumBalance, boolean showAppcoins, boolean showCredits, boolean showEthereum) { + StringBuilder stringBuilder = new StringBuilder(); + String bullet = "\u00A0\u00A0\u00A0\u2022\u00A0\u00A0\u00A0"; + if (showCredits) { + String creditsString = + formatter.formatCurrency(creditsBalance.getValue(), WalletCurrency.CREDITS) + + " " + + WalletCurrency.CREDITS.getSymbol(); + stringBuilder.append(creditsString) + .append(bullet); + } + if (showAppcoins) { + String appcString = + formatter.formatCurrency(appcoinsBalance.getValue(), WalletCurrency.APPCOINS) + + " " + + WalletCurrency.APPCOINS.getSymbol(); + stringBuilder.append(appcString) + .append(bullet); + } + if (showEthereum) { + String ethString = + formatter.formatCurrency(ethereumBalance.getValue(), WalletCurrency.ETHEREUM) + + " " + + WalletCurrency.ETHEREUM.getSymbol(); + stringBuilder.append(ethString) + .append(bullet); + } + String subtitle = stringBuilder.toString(); + if (stringBuilder.length() > bullet.length()) { + subtitle = stringBuilder.substring(0, stringBuilder.length() - bullet.length()); } + return subtitle.replace(bullet, "" + bullet + ""); } - private void onDepositClick(View view, Uri uri) { - viewModel.openDeposit(view.getContext(), uri); + public Observable getEmptyTransactionsScreenClick() { + return emptyTransactionsSubject; + } + + public void navigateToTopApps() { + viewModel.showTopApps(this); + } + + public void navigateToPromotions(boolean clearStack) { + if (clearStack) { + getSupportFragmentManager().popBackStack(); + } + viewModel.navigateToPromotions(this); + } + + public void showPromotionsOverlay() { + getSupportFragmentManager().beginTransaction() + .setCustomAnimations(R.anim.fragment_fade_in_animation, R.anim.fragment_fade_out_animation, + R.anim.fragment_fade_in_animation, R.anim.fragment_fade_out_animation) + .add(R.id.container, OverlayFragment.newInstance(0)) + .addToBackStack(OverlayFragment.class.getName()) + .commit(); + } + + private void dismissNotification(CardNotification cardNotification) { + showScroll = adapter.removeItem(cardNotification); + if (showScroll) { + viewModel.fetchTransactions(false); + } } } \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/UpdateRequiredActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/UpdateRequiredActivity.kt new file mode 100644 index 00000000000..7df3e25de57 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/UpdateRequiredActivity.kt @@ -0,0 +1,53 @@ +package com.asfoundation.wallet.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.asf.wallet.R +import com.asfoundation.wallet.interact.AutoUpdateInteract +import com.google.android.material.snackbar.Snackbar +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.AndroidInjection +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.update_required_main_layout.* +import javax.inject.Inject + +class UpdateRequiredActivity : BaseActivity(), UpdateRequiredView { + + private lateinit var presenter: UpdateRequiredPresenter + + @Inject + lateinit var autoUpdateInteract: AutoUpdateInteract + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + AndroidInjection.inject(this) + setContentView(R.layout.update_required_main_layout) + presenter = UpdateRequiredPresenter(this, CompositeDisposable(), autoUpdateInteract) + presenter.present() + } + + override fun onResume() { + super.onResume() + sendPageViewEvent() + } + + override fun navigateToIntent(intent: Intent) = startActivity(intent) + + override fun updateClick() = RxView.clicks(update_button) + + override fun showError() = + Snackbar.make(main_layout, R.string.unknown_error, Snackbar.LENGTH_SHORT) + + override fun onDestroy() { + super.onDestroy() + presenter.stop() + } + + companion object { + @JvmStatic + fun newIntent(context: Context): Intent { + return Intent(context, UpdateRequiredActivity::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/UpdateRequiredPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/UpdateRequiredPresenter.kt new file mode 100644 index 00000000000..d46b635d559 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/UpdateRequiredPresenter.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.ui + +import com.asfoundation.wallet.interact.AutoUpdateInteract +import io.reactivex.disposables.CompositeDisposable + +class UpdateRequiredPresenter(private val activity: UpdateRequiredView, + private val disposable: CompositeDisposable, + private val autoUpdateInteract: AutoUpdateInteract) { + + fun present() { + handleUpdateClick() + } + + private fun handleUpdateClick() { + disposable.add(activity.updateClick() + .doOnNext { activity.navigateToIntent(autoUpdateInteract.buildUpdateIntent()) } + .subscribe({}, { handleError(it) })) + } + + private fun handleError(throwable: Throwable) { + throwable.printStackTrace() + activity.showError() + } + + fun stop() = disposable.clear() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/UpdateRequiredView.kt b/app/src/main/java/com/asfoundation/wallet/ui/UpdateRequiredView.kt new file mode 100644 index 00000000000..00143b6966a --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/UpdateRequiredView.kt @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.ui + +import android.content.Intent +import com.google.android.material.snackbar.Snackbar +import io.reactivex.Observable + +interface UpdateRequiredView { + + fun navigateToIntent(intent: Intent) + + fun updateClick(): Observable + + fun showError(): Snackbar +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/WalletsActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/WalletsActivity.java deleted file mode 100644 index 70fa4f69aa4..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/WalletsActivity.java +++ /dev/null @@ -1,359 +0,0 @@ -package com.asfoundation.wallet.ui; - -import android.app.Dialog; -import android.arch.lifecycle.ViewModelProviders; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.support.annotation.Nullable; -import android.support.design.widget.BottomSheetBehavior; -import android.support.design.widget.BottomSheetDialog; -import android.support.design.widget.Snackbar; -import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.TextUtils; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.ErrorEnvelope; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.ui.widget.adapter.WalletsAdapter; -import com.asfoundation.wallet.util.KeyboardUtils; -import com.asfoundation.wallet.viewmodel.WalletsViewModel; -import com.asfoundation.wallet.viewmodel.WalletsViewModelFactory; -import com.asfoundation.wallet.widget.AddWalletView; -import com.asfoundation.wallet.widget.BackupView; -import com.asfoundation.wallet.widget.BackupWarningView; -import com.asfoundation.wallet.widget.SystemView; -import dagger.android.AndroidInjection; -import javax.inject.Inject; - -import static com.asfoundation.wallet.C.IMPORT_REQUEST_CODE; -import static com.asfoundation.wallet.C.SHARE_REQUEST_CODE; - -public class WalletsActivity extends BaseActivity - implements View.OnClickListener, AddWalletView.OnNewWalletClickListener, - AddWalletView.OnImportWalletClickListener { - - private final Handler handler = new Handler(); - @Inject WalletsViewModelFactory walletsViewModelFactory; - WalletsViewModel viewModel; - private WalletsAdapter adapter; - private SystemView systemView; - private BackupWarningView backupWarning; - private Dialog dialog; - private boolean isSetDefault; - - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { - AndroidInjection.inject(this); - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_wallets); - // Init toolbar - toolbar(); - - adapter = - new WalletsAdapter(this::onSetWalletDefault, this::onDeleteWallet, this::onExportWallet); - SwipeRefreshLayout refreshLayout = findViewById(R.id.refresh_layout); - systemView = findViewById(R.id.system_view); - backupWarning = findViewById(R.id.backup_warning); - - RecyclerView list = findViewById(R.id.list); - - list.setLayoutManager(new LinearLayoutManager(this)); - list.setAdapter(adapter); - - systemView.attachRecyclerView(list); - systemView.attachSwipeRefreshLayout(refreshLayout); - backupWarning.setOnPositiveClickListener(this::onNowBackup); - backupWarning.setOnNegativeClickListener(this::onLaterBackup); - - viewModel = ViewModelProviders.of(this, walletsViewModelFactory) - .get(WalletsViewModel.class); - - viewModel.error() - .observe(this, this::onError); - viewModel.progress() - .observe(this, systemView::showProgress); - viewModel.wallets() - .observe(this, this::onFetchWallet); - viewModel.defaultWallet() - .observe(this, this::onChangeDefaultWallet); - viewModel.createdWallet() - .observe(this, this::onCreatedWallet); - viewModel.createWalletError() - .observe(this, this::onCreateWalletError); - viewModel.exportedStore() - .observe(this, this::openShareDialog); - viewModel.exportWalletError() - .observe(this, this::onExportWalletError); - viewModel.deleteWalletError() - .observe(this, this::onDeleteWalletError); - - refreshLayout.setOnRefreshListener(viewModel::fetchWallets); - } - - private void onCreateWalletError(ErrorEnvelope errorEnvelope) { - dialog = buildDialog().setTitle(R.string.title_dialog_error) - .setMessage( - TextUtils.isEmpty(errorEnvelope.message) ? getString(R.string.error_create_wallet) - : errorEnvelope.message) - .setPositiveButton(R.string.ok, (dialog, which) -> { - }) - .create(); - dialog.show(); - } - - private void onExportWallet(Wallet wallet) { - showBackupDialog(wallet, false); - } - - @Override public boolean onCreateOptionsMenu(Menu menu) { - if (adapter.getItemCount() > 0) { - getMenuInflater().inflate(R.menu.menu_add, menu); - } - return super.onCreateOptionsMenu(menu); - } - - @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_add: { - onAddWallet(); - } - break; - case android.R.id.home: { - onBackPressed(); - return true; - } - } - return super.onOptionsItemSelected(item); - } - - @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (requestCode == IMPORT_REQUEST_CODE) { - showToolbar(); - if (resultCode == RESULT_OK) { - viewModel.fetchWallets(); - Snackbar.make(systemView, getString(R.string.toast_message_wallet_imported), - Snackbar.LENGTH_SHORT) - .show(); - if (adapter.getItemCount() <= 1) { - viewModel.showTransactions(this); - } - } - } else if (requestCode == SHARE_REQUEST_CODE) { - if (resultCode == RESULT_OK) { - Snackbar.make(systemView, getString(R.string.toast_message_wallet_exported), - Snackbar.LENGTH_SHORT) - .show(); - backupWarning.hide(); - showToolbar(); - hideDialog(); - if (adapter.getItemCount() <= 1) { - onBackPressed(); - } - } else { - dialog = buildDialog().setMessage(R.string.do_manage_make_backup) - .setPositiveButton(R.string.yes_continue, (dialog, which) -> { - hideDialog(); - backupWarning.hide(); - showToolbar(); - if (adapter.getItemCount() <= 1) { - onBackPressed(); - } - }) - .setNegativeButton(R.string.no_repeat, (dialog, which) -> { - openShareDialog(viewModel.exportedStore() - .getValue()); - hideDialog(); - }) - .create(); - dialog.show(); - } - } - } - - @Override public void onBackPressed() { - // User can't start work without wallet. - if (adapter.getItemCount() > 0) { - viewModel.showTransactions(this); - } else { - finish(); - System.exit(0); - } - } - - @Override protected void onPause() { - super.onPause(); - - hideDialog(); - } - - @Override public void onClick(View view) { - switch (view.getId()) { - case R.id.try_again: { - viewModel.fetchWallets(); - } - break; - } - } - - @Override public void onNewWallet(View view) { - hideDialog(); - viewModel.newWallet(); - } - - @Override public void onImportWallet(View view) { - hideDialog(); - viewModel.importWallet(this); - } - - private void onAddWallet() { - AddWalletView addWalletView = new AddWalletView(this); - addWalletView.setOnNewWalletClickListener(this); - addWalletView.setOnImportWalletClickListener(this); - dialog = new BottomSheetDialog(this); - dialog.setContentView(addWalletView); - dialog.setCancelable(true); - dialog.setCanceledOnTouchOutside(true); - BottomSheetBehavior behavior = BottomSheetBehavior.from((View) addWalletView.getParent()); - dialog.setOnShowListener(dialog -> behavior.setPeekHeight(addWalletView.getHeight())); - dialog.show(); - } - - private void onChangeDefaultWallet(Wallet wallet) { - if (isSetDefault) { - viewModel.showTransactions(this); - } else { - adapter.setDefaultWallet(wallet); - } - } - - private void onFetchWallet(Wallet[] wallets) { - if (wallets == null || wallets.length == 0) { - disableDisplayHomeAsUp(); - AddWalletView addWalletView = new AddWalletView(this, R.layout.layout_empty_add_account); - addWalletView.setOnNewWalletClickListener(this); - addWalletView.setOnImportWalletClickListener(this); - systemView.showEmpty(addWalletView); - adapter.setWallets(new Wallet[0]); - hideToolbar(); - } else { - enableDisplayHomeAsUp(); - adapter.setWallets(wallets); - } - invalidateOptionsMenu(); - } - - private void onCreatedWallet(Wallet wallet) { - hideToolbar(); - backupWarning.show(wallet); - } - - private void onLaterBackup(View view, Wallet wallet) { - showNoBackupWarning(wallet); - } - - private void onNowBackup(View view, Wallet wallet) { - showBackupDialog(wallet, true); - } - - private void showNoBackupWarning(Wallet wallet) { - dialog = buildDialog().setTitle(getString(R.string.title_dialog_watch_out)) - .setMessage(getString(R.string.dialog_message_unrecoverable_message)) - .setIcon(R.drawable.ic_warning_black_24dp) - .setPositiveButton(R.string.i_understand, (dialog, whichButton) -> { - backupWarning.hide(); - showToolbar(); - }) - .setNegativeButton(android.R.string.cancel, - null) //(dialog, whichButton) -> showBackupDialog(wallet, true) - .create(); - dialog.show(); - } - - private void showBackupDialog(Wallet wallet, boolean isNew) { - BackupView view = new BackupView(this); - dialog = buildDialog().setView(view) - .setPositiveButton(R.string.ok, (dialogInterface, i) -> { - viewModel.exportWallet(wallet, view.getPassword()); - KeyboardUtils.hideKeyboard(view.findViewById(R.id.password)); - }) - .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { - if (isNew) { - onCreatedWallet(wallet); - } - KeyboardUtils.hideKeyboard(view.findViewById(R.id.password)); - }) - .setOnDismissListener( - dialog -> KeyboardUtils.hideKeyboard(view.findViewById(R.id.password))) - .create(); - dialog.show(); - handler.postDelayed(() -> KeyboardUtils.showKeyboard(view.findViewById(R.id.password)), 500); - } - - private void openShareDialog(String jsonData) { - Intent sharingIntent = new Intent(Intent.ACTION_SEND); - sharingIntent.setType("text/plain"); - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, "Keystore"); - sharingIntent.putExtra(Intent.EXTRA_TEXT, jsonData); - startActivityForResult(Intent.createChooser(sharingIntent, "Share via"), SHARE_REQUEST_CODE); - } - - private void onExportWalletError(ErrorEnvelope errorEnvelope) { - dialog = buildDialog().setTitle(R.string.title_dialog_error) - .setMessage(TextUtils.isEmpty(errorEnvelope.message) ? getString(R.string.error_export) - : errorEnvelope.message) - .setPositiveButton(R.string.ok, (dialogInterface, with) -> { - }) - .create(); - dialog.show(); - } - - private void onDeleteWalletError(ErrorEnvelope errorEnvelope) { - dialog = buildDialog().setTitle(R.string.title_dialog_error) - .setMessage( - TextUtils.isEmpty(errorEnvelope.message) ? getString(R.string.error_deleting_account) - : errorEnvelope.message) - .setPositiveButton(R.string.ok, (dialogInterface, with) -> { - }) - .create(); - dialog.show(); - } - - private void onError(ErrorEnvelope errorEnvelope) { - systemView.showError(errorEnvelope.message, this); - } - - private void onSetWalletDefault(Wallet wallet) { - viewModel.setDefaultWallet(wallet); - isSetDefault = true; - } - - private void onDeleteWallet(Wallet wallet) { - dialog = buildDialog().setTitle(getString(R.string.title_delete_account)) - .setMessage(getString(R.string.confirm_delete_account)) - .setIcon(R.drawable.ic_warning_black_24dp) - .setPositiveButton(android.R.string.yes, (dialog, btn) -> viewModel.deleteWallet(wallet)) - .setNegativeButton(android.R.string.no, null) - .create(); - dialog.show(); - } - - private AlertDialog.Builder buildDialog() { - hideDialog(); - return new AlertDialog.Builder(this); - } - - private void hideDialog() { - if (dialog != null && dialog.isShowing()) { - dialog.dismiss(); - dialog = null; - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropActivity.java index cb0c98967af..5600b71799e 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropActivity.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropActivity.java @@ -3,8 +3,8 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.support.annotation.Nullable; import android.view.MenuItem; +import androidx.annotation.Nullable; import com.asf.wallet.R; import com.asfoundation.wallet.ui.BaseActivity; @@ -26,10 +26,8 @@ public static Intent newIntent(Context context) { } @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: { - finish(); - } + if (item.getItemId() == android.R.id.home) { + finish(); } return super.onOptionsItemSelected(item); } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropChainIdMapper.java b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropChainIdMapper.java index a7f8f65d46c..f20f1953286 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropChainIdMapper.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropChainIdMapper.java @@ -1,27 +1,23 @@ package com.asfoundation.wallet.ui.airdrop; import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.interact.FindDefaultNetworkInteract; import io.reactivex.Single; public class AirdropChainIdMapper { - private final FindDefaultNetworkInteract defaultNetworkInteract; + private final NetworkInfo defaultNetwork; - public AirdropChainIdMapper(FindDefaultNetworkInteract defaultNetworkInteract) { - this.defaultNetworkInteract = defaultNetworkInteract; + public AirdropChainIdMapper(NetworkInfo defaultNetwork) { + this.defaultNetwork = defaultNetwork; } public Single getAirdropChainId() { - return defaultNetworkInteract.find() - .map(this::map); + return Single.just(map(defaultNetwork)); } private Integer map(NetworkInfo networkInfo) { - switch (networkInfo.chainId) { - case 1: - return 1; - default: - return 3; + if (networkInfo.chainId == 1) { + return 1; } + return 3; } } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropFragment.java b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropFragment.java index c17b2d224a4..cc86ccd9a99 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropFragment.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropFragment.java @@ -6,9 +6,6 @@ import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.TextInputEditText; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; @@ -17,9 +14,14 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.asf.wallet.R; +import com.asfoundation.wallet.GlideApp; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.textfield.TextInputEditText; import com.jakewharton.rxbinding2.view.RxView; -import com.squareup.picasso.Picasso; import dagger.android.support.DaggerFragment; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -103,10 +105,10 @@ private void dismissDialog(Dialog dialog) { @Override public void showCaptcha(String captchaUrl) { Log.d(TAG, "showCaptcha() called with: captchaUrl = [" + captchaUrl + "]"); - Picasso.with(getContext()) - .invalidate(captchaUrl); - Picasso.with(getContext()) + GlideApp.with(getContext()) .load(captchaUrl) + .apply(new RequestOptions().diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true)) .into(captchaView); } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropInteractor.java b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropInteractor.java index ae4845b40c9..c3b755e8d3d 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropInteractor.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropInteractor.java @@ -3,7 +3,6 @@ import com.asfoundation.wallet.Airdrop; import com.asfoundation.wallet.AirdropData; import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.repository.EthereumNetworkRepositoryType; import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; @@ -13,40 +12,32 @@ public class AirdropInteractor { private final Airdrop airdrop; private final FindDefaultWalletInteract findDefaultWalletInteract; private final AirdropChainIdMapper airdropChainIdMapper; - private final EthereumNetworkRepositoryType repository; public AirdropInteractor(Airdrop airdrop, FindDefaultWalletInteract findDefaultWalletInteract, - AirdropChainIdMapper airdropChainIdMapper, EthereumNetworkRepositoryType repository) { + AirdropChainIdMapper airdropChainIdMapper) { this.airdrop = airdrop; this.findDefaultWalletInteract = findDefaultWalletInteract; this.airdropChainIdMapper = airdropChainIdMapper; - this.repository = repository; } - public Single requestCaptcha() { + Single requestCaptcha() { return findDefaultWalletInteract.find() .observeOn(Schedulers.io()) .flatMap(wallet -> airdrop.requestCaptcha(wallet.address)); } - public Completable requestAirdrop(String captchaAnswer) { + Completable requestAirdrop(String captchaAnswer) { return findDefaultWalletInteract.find() .flatMap(wallet -> airdropChainIdMapper.getAirdropChainId() .doOnSuccess(chainId -> airdrop.request(wallet.address, chainId, captchaAnswer))) - .toCompletable(); + .ignoreElement(); } public Observable getStatus() { - return airdrop.getStatus() - .doOnNext(airdropData -> { - if (airdropData.getStatus() - .equals(AirdropData.AirdropStatus.SUCCESS)) { - repository.setDefaultNetworkInfo(airdropData.getNetworkId()); - } - }); + return airdrop.getStatus(); } - public void terminateStateConsumed() { + void terminateStateConsumed() { airdrop.resetState(); } } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropPresenter.java b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropPresenter.java index c5c2d9663bb..263866d9fc5 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropPresenter.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropPresenter.java @@ -11,8 +11,8 @@ public class AirdropPresenter { private final AirdropInteractor airdrop; private final Scheduler scheduler; - public AirdropPresenter(AirdropView view, CompositeDisposable disposables, - AirdropInteractor airdrop, Scheduler scheduler) { + AirdropPresenter(AirdropView view, CompositeDisposable disposables, AirdropInteractor airdrop, + Scheduler scheduler) { this.view = view; this.disposables = disposables; this.airdrop = airdrop; @@ -60,7 +60,7 @@ private Single refreshCaptcha() { return airdrop.requestCaptcha() .observeOn(scheduler) .doOnSuccess(view::showCaptcha) - .doOnError(throwable -> throwable.printStackTrace()); + .doOnError(Throwable::printStackTrace); } private void onAirdropStatusChange() { diff --git a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropView.java b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropView.java index 4dbd1329cca..58a362a09bf 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropView.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AirdropView.java @@ -1,7 +1,6 @@ package com.asfoundation.wallet.ui.airdrop; import io.reactivex.Observable; -import io.reactivex.disposables.Disposable; interface AirdropView { void showCaptcha(String captchaUrl); diff --git a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AppcoinsTransactionService.java b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AppcoinsTransactionService.java index 5d8097951af..1e7d842718f 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AppcoinsTransactionService.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/airdrop/AppcoinsTransactionService.java @@ -14,8 +14,8 @@ public AppcoinsTransactionService(PendingTransactionService pendingTransactionSe this.pendingTransactionService = pendingTransactionService; } - @Override public Completable waitForTransactionToComplete(String transactionHash, int chainId) { - return pendingTransactionService.checkTransactionState(transactionHash, chainId) + @Override public Completable waitForTransactionToComplete(String transactionHash) { + return pendingTransactionService.checkTransactionState(transactionHash) .retryWhen(throwableObservable -> throwableObservable.flatMap(throwable -> { if (throwable instanceof TransactionNotFoundException) { return Observable.timer(5, TimeUnit.SECONDS); diff --git a/app/src/main/java/com/asfoundation/wallet/ui/appcoins/AppcoinsApplicationAdapter.java b/app/src/main/java/com/asfoundation/wallet/ui/appcoins/AppcoinsApplicationAdapter.java new file mode 100644 index 00000000000..d9dd9194640 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/appcoins/AppcoinsApplicationAdapter.java @@ -0,0 +1,46 @@ +package com.asfoundation.wallet.ui.appcoins; + +import android.view.LayoutInflater; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.asf.wallet.R; +import com.asfoundation.wallet.ui.appcoins.applications.AppcoinsApplication; +import com.asfoundation.wallet.ui.widget.holder.AppcoinsApplicationViewHolder; +import com.asfoundation.wallet.ui.widget.holder.ApplicationClickAction; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import rx.functions.Action2; + +public class AppcoinsApplicationAdapter + extends RecyclerView.Adapter { + private final Action2 applicationClickListener; + private List applications; + + public AppcoinsApplicationAdapter( + Action2 applicationClickListener, + @NotNull List applications) { + this.applicationClickListener = applicationClickListener; + this.applications = applications; + } + + @NonNull @Override + public AppcoinsApplicationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new AppcoinsApplicationViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_appcoins_application, parent, false), applicationClickListener); + } + + @Override + public void onBindViewHolder(@NonNull AppcoinsApplicationViewHolder holder, int position) { + holder.bind(applications.get(position)); + } + + @Override public int getItemCount() { + return applications.size(); + } + + public void setApplications(List applications) { + this.applications = applications; + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/appcoins/CardNotificationsItemDecorator.kt b/app/src/main/java/com/asfoundation/wallet/ui/appcoins/CardNotificationsItemDecorator.kt new file mode 100644 index 00000000000..5fc14bf783d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/appcoins/CardNotificationsItemDecorator.kt @@ -0,0 +1,69 @@ +package com.asfoundation.wallet.ui.appcoins + +import android.graphics.Rect +import android.util.TypedValue +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import kotlin.math.max + + +class CardNotificationsItemDecorator : RecyclerView.ItemDecoration() { + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, + state: RecyclerView.State) { + if (parent.adapter != null && parent.adapter!!.itemCount > 1) { + if (parent.getChildAdapterPosition(view) == 0) { + if (isRtl(view)) { + outRect.right = 8 + outRect.left = 0 + } else { + outRect.right = 0 + outRect.left = 8 + } + } + + if (parent.getChildAdapterPosition(view) == state.itemCount - 1) { + if (isRtl(view)) { + outRect.right = 0 + outRect.left = 8 + } else { + outRect.right = 8 + outRect.left = 0 + } + } + } else { + outRect.right = 16 + outRect.left = 16 + + val maxWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 400f, + parent.context.resources + .displayMetrics) + .toInt() + + val margins = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32f, + parent.context.resources + .displayMetrics) + .toInt() + + val screenWidth = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, parent.measuredWidth.toFloat(), + parent.context.resources + .displayMetrics) + .toInt() + + val cardWidth = if (screenWidth > maxWidth) { + maxWidth + } else { + screenWidth - margins + } + + var sidePadding = (screenWidth - cardWidth) / 2 + sidePadding = max(0, sidePadding) + outRect.set(sidePadding, 0, sidePadding, 0) + } + } + + private fun isRtl(view: View) = + view.context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/appcoins/ItemDecorator.java b/app/src/main/java/com/asfoundation/wallet/ui/appcoins/ItemDecorator.java new file mode 100644 index 00000000000..84e6856d3ab --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/appcoins/ItemDecorator.java @@ -0,0 +1,44 @@ +package com.asfoundation.wallet.ui.appcoins; + +import android.graphics.Rect; +import androidx.recyclerview.widget.RecyclerView; +import android.view.View; + +public class ItemDecorator extends RecyclerView.ItemDecoration { + private final int spaceInDp; + + public ItemDecorator(int spaceInDp) { + this.spaceInDp = spaceInDp; + } + + @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + + if (parent.getChildAdapterPosition(view) == 0) { + if (isRtl(view)) { + outRect.right = spaceInDp; + outRect.left = 0; + } else { + outRect.right = 0; + outRect.left = spaceInDp; + } + } + + if (parent.getChildAdapterPosition(view) == state.getItemCount() - 1) { + if (isRtl(view)) { + outRect.right = 0; + outRect.left = spaceInDp; + } else { + outRect.right = spaceInDp; + outRect.left = 0; //don't forget about recycling... + } + } + } + + private boolean isRtl(View view) { + return view.getContext() + .getResources() + .getConfiguration() + .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/appcoins/applications/AppcoinsApplication.java b/app/src/main/java/com/asfoundation/wallet/ui/appcoins/applications/AppcoinsApplication.java new file mode 100644 index 00000000000..b8736d0ff71 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/appcoins/applications/AppcoinsApplication.java @@ -0,0 +1,84 @@ +package com.asfoundation.wallet.ui.appcoins.applications; + +import java.util.Objects; + +public class AppcoinsApplication { + + private final String name; + private final double rating; + private final String iconUrl; + private final String featuredGraphic; + private final String packageName; + private final String uniqueName; + + public AppcoinsApplication(String name, double rating, String iconUrl, String featuredGraphic, + String packageName, String uniqueName) { + this.name = name; + this.rating = rating; + this.iconUrl = iconUrl; + this.featuredGraphic = featuredGraphic; + this.packageName = packageName; + this.uniqueName = uniqueName; + } + + @Override public int hashCode() { + int result; + long temp; + result = name.hashCode(); + temp = Double.doubleToLongBits(rating); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + iconUrl.hashCode(); + result = 31 * result + featuredGraphic.hashCode(); + return result; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AppcoinsApplication)) return false; + + AppcoinsApplication that = (AppcoinsApplication) o; + + if (Double.compare(that.rating, rating) != 0) return false; + if (!Objects.equals(name, that.name)) return false; + if (!Objects.equals(iconUrl, that.iconUrl)) return false; + if (!Objects.equals(featuredGraphic, that.featuredGraphic)) return false; + return Objects.equals(packageName, that.packageName); + } + + @Override public String toString() { + return "AppcoinsApplication{" + + "name='" + + name + + '\'' + + ", rating=" + + rating + + ", iconUrl='" + + iconUrl + + '\'' + + '}'; + } + + public String getName() { + return name; + } + + public double getRating() { + return rating; + } + + public String getIcon() { + return iconUrl; + } + + public String getFeaturedGraphic() { + return featuredGraphic; + } + + public String getUniqueName() { + return uniqueName; + } + + public String getPackageName() { + return packageName; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupActivityPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupActivityPresenter.kt new file mode 100644 index 00000000000..511fc2fb4fb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupActivityPresenter.kt @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.ui.backup + +class BackupActivityPresenter(private val view: BackupActivityView) { + + var currentFragmentName: String = BackupWalletFragment::class.java.simpleName; + + fun present(isCreating: Boolean) { + if (isCreating) view.showBackupScreen() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupActivityView.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupActivityView.kt new file mode 100644 index 00000000000..936b3be9183 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupActivityView.kt @@ -0,0 +1,22 @@ +package com.asfoundation.wallet.ui.backup + +import io.reactivex.Observable + +interface BackupActivityView { + + fun showBackupScreen() + + fun showBackupCreationScreen(password: String) + + fun askForWritePermissions() + + fun showSuccessScreen() + + fun closeScreen() + + fun onPermissionGiven(): Observable + + fun openSystemFileDirectory(fileName: String) + + fun onSystemFileIntentResult(): Observable +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupCreationFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupCreationFragment.kt new file mode 100644 index 00000000000..fb7c1e4e55c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupCreationFragment.kt @@ -0,0 +1,189 @@ +package com.asfoundation.wallet.ui.backup + +import android.content.Context +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.app.ShareCompat +import com.asf.wallet.R +import com.asfoundation.wallet.backup.FileInteractor +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.interact.ExportWalletInteract +import com.asfoundation.wallet.logging.Logger +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.backup_dialog.view.* +import kotlinx.android.synthetic.main.fragment_backup_creation_layout.* +import javax.inject.Inject + +class BackupCreationFragment : BackupCreationView, DaggerFragment() { + + @Inject + lateinit var exportWalletInteract: ExportWalletInteract + + @Inject + lateinit var fileInteractor: FileInteractor + + @Inject + lateinit var walletsEventSender: WalletsEventSender + + @Inject + lateinit var logger: Logger + + private lateinit var dialogView: View + private lateinit var presenter: BackupCreationPresenter + private lateinit var activityView: BackupActivityView + private lateinit var dialog: AlertDialog + + companion object { + + private const val WALLET_ADDRESS_KEY = "wallet_address" + private const val PASSWORD_KEY = "password" + + @JvmStatic + fun newInstance(walletAddress: String, password: String): BackupCreationFragment { + val fragment = BackupCreationFragment() + fragment.arguments = Bundle().apply { + putString(WALLET_ADDRESS_KEY, walletAddress) + putString(PASSWORD_KEY, password) + } + return fragment + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check(context is BackupActivityView) { "Backup fragment must be attached to Backup activity" } + activityView = context + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = + BackupCreationPresenter(activityView, this, exportWalletInteract, fileInteractor, + walletsEventSender, logger, + Schedulers.io(), AndroidSchedulers.mainThread(), CompositeDisposable(), walletAddress, + password, fileInteractor.getTemporaryPath(), fileInteractor.getDownloadPath()) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + dialogView = layoutInflater.inflate(R.layout.backup_dialog, null) + return inflater.inflate(R.layout.fragment_backup_creation_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + proceed_button.visibility = View.VISIBLE //To avoid flick when user navigates with open keyboard + presenter.present(savedInstanceState) + animation.playAnimation() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + presenter.onSaveInstanceState(outState) + } + + override fun onResume() { + super.onResume() + presenter.onResume() + } + + override fun onDestroy() { + presenter.stop() + super.onDestroy() + } + + override fun shareFile(uri: Uri) { + activity?.let { + ShareCompat.IntentBuilder.from(it) + .setStream(uri) + .setType("text/json") + .setSubject(getString(R.string.tab_keystore)) + .setChooserTitle(R.string.share_via) + .startChooser() + } + } + + override fun getFirstSaveClick() = RxView.clicks(proceed_button) + + override fun getFinishClick(): Observable = RxView.clicks(finish_button) + + override fun getSaveAgainClick() = RxView.clicks(save_again_button) + + override fun enableSaveButton() { + proceed_button.isEnabled = true + animation.cancelAnimation() + } + + override fun showError() { + Toast.makeText(context, R.string.error_export, Toast.LENGTH_LONG) + .show() + activityView.closeScreen() + } + + override fun showSaveOnDeviceDialog(defaultName: String, path: String?) { + if (!(::dialog.isInitialized)) { + dialog = AlertDialog.Builder(context!!) + .setView(dialogView) + .create() + dialog.window?.decorView?.setBackgroundResource(R.color.transparent) + dialogView.visibility = View.VISIBLE + dialogView.edit_text_name?.setText(defaultName) + path?.let { + dialogView.store_path?.text = it + dialogView.store_path?.visibility = View.VISIBLE + } + } + dialog.show() + } + + override fun showConfirmation() { + animation.visibility = View.INVISIBLE + backup_confirmation_image.setImageResource(R.drawable.ic_backup_confirm) + backup_confirmation_image.visibility = View.VISIBLE + title.text = getString(R.string.backup_done_body) + description.visibility = View.INVISIBLE + proceed_button.visibility = View.INVISIBLE + file_shared_buttons.visibility = View.VISIBLE + //Fix for bug related with group layout + file_shared_buttons.requestLayout() + } + + override fun getDialogCancelClick() = RxView.clicks(dialogView.backup_cancel) + + override fun getDialogSaveClick(): Observable { + return RxView.clicks(dialogView.backup_save) + .map { dialogView.edit_text_name.text.toString() } + } + + override fun closeDialog() { + if (::dialog.isInitialized) { + dialog.cancel() + } + } + + private val walletAddress: String by lazy { + if (arguments!!.containsKey(WALLET_ADDRESS_KEY)) { + arguments!!.getString(WALLET_ADDRESS_KEY)!! + } else { + throw IllegalArgumentException("Wallet address not available") + } + } + + private val password: String by lazy { + if (arguments!!.containsKey(PASSWORD_KEY)) { + arguments!!.getString(PASSWORD_KEY)!! + } else { + throw IllegalArgumentException("Password not available") + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupCreationPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupCreationPresenter.kt new file mode 100644 index 00000000000..046de59cca4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupCreationPresenter.kt @@ -0,0 +1,212 @@ +package com.asfoundation.wallet.ui.backup + +import android.os.Build +import android.os.Bundle +import androidx.documentfile.provider.DocumentFile +import com.asfoundation.wallet.backup.FileInteractor +import com.asfoundation.wallet.billing.analytics.WalletsAnalytics +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.interact.ExportWalletInteract +import com.asfoundation.wallet.logging.Logger +import io.reactivex.Completable +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import java.io.File + +class BackupCreationPresenter( + private val activityView: BackupActivityView, + private val view: BackupCreationView, + private val exportWalletInteract: ExportWalletInteract, + private val fileInteractor: FileInteractor, + private val walletsEventSender: WalletsEventSender, + private val logger: Logger, + private val networkScheduler: Scheduler, + private val viewScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val walletAddress: String, + private val password: String, + private val temporaryPath: File?, + private val downloadsPath: File?) { + + companion object { + private const val FILE_SHARED_KEY = "FILE_SHARED" + private const val KEYSTORE_KEY = "KEYSTORE" + private const val FILE_NAME_KEY = "FILE_NAME" + private val TAG = BackupCreationPresenter::class.java.name + } + + private var fileShared = false + private var cachedKeystore = "" + private var cachedFileName: String? = null + + fun present(savedInstance: Bundle?) { + savedInstance?.let { + fileShared = it.getBoolean(FILE_SHARED_KEY) + cachedKeystore = it.getString(KEYSTORE_KEY, "") + cachedFileName = it.getString(FILE_NAME_KEY) + } + createBackUpFile() + handleFinishClick() + handleFirstSaveClick() + handleSaveAgainClick() + handlePermissionGiven() + handleDialogCancelClick() + handleDialogSaveClick() + handleSystemFileIntentResult() + } + + private fun handleSystemFileIntentResult() { + disposables.add(activityView.onSystemFileIntentResult() + .observeOn(networkScheduler) + .flatMapCompletable { + if (it.documentFile != null && cachedFileName != null) { + createAndSaveFile(it.documentFile, cachedFileName!!) + } else { + Completable.fromAction { view.closeDialog() } + } + } + .subscribe({}, { showError(it) })) + } + + private fun handleDialogSaveClick() { + disposables.add(view.getDialogSaveClick() + .doOnNext { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + cachedFileName = it + activityView.openSystemFileDirectory(it) + } else { + handleDialogSaveClickBelowAndroidQ(it) + } + walletsEventSender.sendWalletSaveFileEvent(WalletsAnalytics.ACTION_SAVE, + WalletsAnalytics.STATUS_SUCCESS) + } + .doOnError { + walletsEventSender.sendWalletSaveFileEvent(WalletsAnalytics.ACTION_SAVE, + WalletsAnalytics.STATUS_FAIL, it.message) + } + .subscribe({}, { showError(it) })) + } + + private fun handleDialogSaveClickBelowAndroidQ(fileName: String) { + disposables.add(fileInteractor.createAndSaveFile(cachedKeystore, downloadsPath, fileName) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnComplete { + view.closeDialog() + activityView.showSuccessScreen() + } + .doOnError { showError(it) } + .subscribe({}, { showError(it) })) + } + + private fun handleDialogCancelClick() { + disposables.add(view.getDialogCancelClick() + .doOnNext { view.closeDialog() } + .doOnNext { + walletsEventSender.sendWalletSaveFileEvent(WalletsAnalytics.ACTION_CANCEL, + WalletsAnalytics.STATUS_FAIL) + } + .doOnError { t -> + walletsEventSender.sendWalletSaveFileEvent(WalletsAnalytics.ACTION_CANCEL, + WalletsAnalytics.STATUS_FAIL, t.message) + } + .subscribe({}, { view.closeDialog() })) + } + + private fun createBackUpFile() { + disposables.add(exportWalletInteract.export(walletAddress, password) + .doOnSuccess { cachedKeystore = it } + .flatMapCompletable { fileInteractor.createTmpFile(walletAddress, it, temporaryPath) } + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnComplete { view.enableSaveButton() } + .doOnError { showError(it) } + .subscribe({}, { showError(it) })) + } + + private fun handleFinishClick() { + disposables.add(view.getFinishClick() + .observeOn(viewScheduler) + .doOnNext { + walletsEventSender.sendWalletConfirmationBackupEvent(WalletsAnalytics.ACTION_FINISH) + activityView.closeScreen() + } + .subscribe({}, { activityView.closeScreen() }) + ) + } + + private fun handleFirstSaveClick() { + disposables.add(view.getFirstSaveClick() + .observeOn(viewScheduler) + .doOnNext { shareFile(fileInteractor.getCachedFile()) } + .subscribe({}, { logger.log(TAG, it) })) + } + + private fun shareFile(file: File?) { + if (file == null) { + showError("Error retrieving file") + } else { + fileShared = true + view.shareFile(fileInteractor.getUriFromFile(file)) + walletsEventSender.sendSaveBackupEvent(WalletsAnalytics.ACTION_SAVE) + } + } + + private fun handleSaveAgainClick() { + disposables.add(view.getSaveAgainClick() + .doOnNext { activityView.askForWritePermissions() } + .doOnNext { + walletsEventSender.sendWalletConfirmationBackupEvent(WalletsAnalytics.ACTION_SAVE) + } + .subscribe({}, { logger.log(TAG, it) })) + } + + private fun handlePermissionGiven() { + disposables.add(activityView.onPermissionGiven() + .doOnNext { + view.showSaveOnDeviceDialog(fileInteractor.getDefaultBackupFileName(walletAddress), + downloadsPath?.path) + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun createAndSaveFile(documentFile: DocumentFile, + fileName: String): Completable { + return fileInteractor.createAndSaveFile(cachedKeystore, documentFile, fileName) + .observeOn(viewScheduler) + .doOnComplete { + fileInteractor.saveChosenUri(documentFile.uri) + view.closeDialog() + activityView.showSuccessScreen() + } + } + + fun onResume() { + if (fileShared) { + view.showConfirmation() + fileInteractor.deleteFile() + } + } + + fun stop() { + fileInteractor.deleteFile() + disposables.clear() + } + + fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(FILE_SHARED_KEY, fileShared) + outState.putString(KEYSTORE_KEY, cachedKeystore) + outState.putString(FILE_NAME_KEY, cachedFileName) + } + + private fun showError(throwable: Throwable) { + throwable.printStackTrace() + logger.log(TAG, throwable) + view.showError() + } + + private fun showError(message: String) { + logger.log(TAG, message) + view.showError() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupCreationView.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupCreationView.kt new file mode 100644 index 00000000000..f0f7037c4b0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupCreationView.kt @@ -0,0 +1,29 @@ +package com.asfoundation.wallet.ui.backup + +import android.net.Uri +import io.reactivex.Observable + +interface BackupCreationView { + + fun shareFile(uri: Uri) + + fun getFirstSaveClick(): Observable + + fun getSaveAgainClick(): Observable + + fun getFinishClick(): Observable + + fun showConfirmation() + + fun enableSaveButton() + + fun showError() + + fun showSaveOnDeviceDialog(defaultName: String, path: String?) + + fun getDialogCancelClick(): Observable + + fun getDialogSaveClick(): Observable + + fun closeDialog() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupSuccessFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupSuccessFragment.kt new file mode 100644 index 00000000000..23aded2fb11 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupSuccessFragment.kt @@ -0,0 +1,59 @@ +package com.asfoundation.wallet.ui.backup + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.fragment_backup_creation_layout.animation +import kotlinx.android.synthetic.main.fragment_backup_success_layout.* + +class BackupSuccessFragment : DaggerFragment(), BackupSuccessFragmentView { + + private lateinit var presenter: BackupSuccessPresenter + private lateinit var activityView: BackupActivityView + + companion object { + @JvmStatic + fun newInstance() = BackupSuccessFragment() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = BackupSuccessPresenter(this, activityView, CompositeDisposable()) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_backup_success_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present() + animation.playAnimation() + val text = "${getString(R.string.backup_confirmation_tips_title)}\n\n• ${getString( + R.string.backup_confirmation_tips_1)}\n• ${getString( + R.string.backup_confirmation_tips_2)}\n• ${getString( + R.string.backup_confirmation_tips_3)}" + information.text = text + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check( + context is BackupActivityView) { "BackupSuccess fragment must be attached to Backup activity" } + activityView = context + } + + override fun getCloseButtonClick() = RxView.clicks(close_btn) + + override fun onDestroy() { + presenter.stop() + super.onDestroy() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupSuccessFragmentView.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupSuccessFragmentView.kt new file mode 100644 index 00000000000..8956699d385 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupSuccessFragmentView.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.ui.backup + +import io.reactivex.Observable + +interface BackupSuccessFragmentView { + fun getCloseButtonClick(): Observable +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupSuccessPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupSuccessPresenter.kt new file mode 100644 index 00000000000..f1cc26548a3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupSuccessPresenter.kt @@ -0,0 +1,20 @@ +package com.asfoundation.wallet.ui.backup + +import io.reactivex.disposables.CompositeDisposable + +class BackupSuccessPresenter(private val view: BackupSuccessFragmentView, + private val activityView: BackupActivityView, + private val disposables: CompositeDisposable) { + + fun present() { + handleCloseBtnClick() + } + + private fun handleCloseBtnClick() { + disposables.add(view.getCloseButtonClick() + .doOnNext { activityView.closeScreen() } + .subscribe()) + } + + fun stop() = disposables.clear() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupWalletFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupWalletFragment.kt new file mode 100644 index 00000000000..0fa4a9e92ea --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupWalletFragment.kt @@ -0,0 +1,96 @@ +package com.asfoundation.wallet.ui.backup + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import com.asf.wallet.R +import com.asfoundation.wallet.ui.balance.BalanceInteract +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.fragment_backup_wallet_layout.* +import kotlinx.android.synthetic.main.item_wallet_addr.* +import javax.inject.Inject + +class BackupWalletFragment : DaggerFragment(), BackupWalletFragmentView { + + @Inject + lateinit var balanceInteract: BalanceInteract + + @Inject + lateinit var currencyFormatter: CurrencyFormatUtils + private lateinit var presenter: BackupWalletPresenter + private lateinit var activityView: BackupActivityView + + companion object { + private const val PARAM_WALLET_ADDR = "PARAM_WALLET_ADDR" + + @JvmStatic + fun newInstance(walletAddress: String): BackupWalletFragment { + val bundle = Bundle() + bundle.putString(PARAM_WALLET_ADDR, walletAddress) + val fragment = BackupWalletFragment() + fragment.arguments = bundle + return fragment + } + } + + private val walletAddress: String by lazy { + if (arguments!!.containsKey(PARAM_WALLET_ADDR)) { + arguments!!.getString(PARAM_WALLET_ADDR)!! + } else { + throw IllegalArgumentException("Wallet address not available") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = + BackupWalletPresenter(balanceInteract, this, activityView, + CompositeDisposable(), Schedulers.io(), AndroidSchedulers.mainThread()) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_backup_wallet_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present(walletAddress) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check( + context is BackupActivityView) { "BackupWallet fragment must be attached to Backup activity" } + activityView = context + } + + override fun showBalance(value: FiatValue) { + address.text = walletAddress + amount.text = + getString(R.string.value_fiat, value.symbol, currencyFormatter.formatCurrency(value.amount)) + } + + override fun getBackupClick(): Observable = RxView.clicks(backup_btn) + .map { password.text.toString() } + + override fun hideKeyboard() { + val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.hideSoftInputFromWindow(password.windowToken, 0) + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupWalletFragmentView.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupWalletFragmentView.kt new file mode 100644 index 00000000000..e073f96f322 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupWalletFragmentView.kt @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.ui.backup + +import com.asfoundation.wallet.ui.iab.FiatValue +import io.reactivex.Observable + +interface BackupWalletFragmentView { + fun showBalance(value: FiatValue) + fun getBackupClick(): Observable + fun hideKeyboard() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupWalletPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupWalletPresenter.kt new file mode 100644 index 00000000000..68f7f631f81 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/BackupWalletPresenter.kt @@ -0,0 +1,37 @@ +package com.asfoundation.wallet.ui.backup + +import com.asfoundation.wallet.ui.balance.BalanceInteract +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +class BackupWalletPresenter(private var balanceInteract: BalanceInteract, + private var view: BackupWalletFragmentView, + private var activityView: BackupActivityView, + private var disposables: CompositeDisposable, + private var dbScheduler: Scheduler, + private var viewScheduler: Scheduler) { + + fun present(walletAddress: String) { + handleBackupClick() + retrieveStoredBalance(walletAddress) + } + + private fun handleBackupClick() { + disposables.add(view.getBackupClick() + .doOnNext { + view.hideKeyboard() + activityView.showBackupCreationScreen(it) + } + .subscribe()) + } + + private fun retrieveStoredBalance(walletAddress: String) { + disposables.add(balanceInteract.getStoredOverallBalance(walletAddress) + .subscribeOn(dbScheduler) + .observeOn(viewScheduler) + .map { view.showBalance(it) } + .subscribe()) + } + + fun stop() = disposables.clear() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/SystemFileIntentResult.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/SystemFileIntentResult.kt new file mode 100644 index 00000000000..7677aab4aed --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/SystemFileIntentResult.kt @@ -0,0 +1,5 @@ +package com.asfoundation.wallet.ui.backup + +import androidx.documentfile.provider.DocumentFile + +data class SystemFileIntentResult(val documentFile: DocumentFile? = null) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/backup/WalletBackupActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/backup/WalletBackupActivity.kt new file mode 100644 index 00000000000..20532085947 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/backup/WalletBackupActivity.kt @@ -0,0 +1,167 @@ +package com.asfoundation.wallet.ui.backup + +import android.Manifest +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.view.MenuItem +import androidx.core.app.ActivityCompat +import androidx.documentfile.provider.DocumentFile +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.WalletsAnalytics +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.permissions.manage.view.ToolbarManager +import com.asfoundation.wallet.ui.BaseActivity +import com.google.android.material.snackbar.Snackbar +import dagger.android.AndroidInjection +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.activity_backup.* +import javax.inject.Inject + + +class WalletBackupActivity : BaseActivity(), BackupActivityView, ToolbarManager { + + companion object { + @JvmStatic + fun newIntent(context: Context, walletAddress: String) = + Intent(context, WalletBackupActivity::class.java).apply { + putExtra(WALLET_ADDRESS, walletAddress) + } + + private const val WALLET_ADDRESS = "wallet_addr" + private const val FILE_NAME_EXTRA_KEY = "file_name" + private const val RC_WRITE_EXTERNAL_STORAGE_PERMISSION = 1000 + private const val ACTION_OPEN_DOCUMENT_TREE_REQUEST_CODE = 1001 + + } + + @Inject + lateinit var walletsEventSender: WalletsEventSender + private lateinit var presenter: BackupActivityPresenter + private var onPermissionSubject: PublishSubject? = null + private var onDocumentFileSubject: PublishSubject? = null + + private val walletAddress: String by lazy { + if (intent.extras!!.containsKey(WALLET_ADDRESS)) { + intent.extras!!.getString(WALLET_ADDRESS)!! + } else { + throw IllegalArgumentException("Wallet address not found") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_backup) + onPermissionSubject = PublishSubject.create() + onDocumentFileSubject = PublishSubject.create() + presenter = BackupActivityPresenter(this) + presenter.present(savedInstanceState == null) + savedInstanceState?.let { setupToolbar() } + } + + override fun showBackupScreen() { + presenter.currentFragmentName = BackupWalletFragment::class.java.simpleName + setupToolbar() + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, BackupWalletFragment.newInstance(walletAddress)) + .commit() + } + + override fun showBackupCreationScreen(password: String) { + presenter.currentFragmentName = BackupCreationFragment::class.java.simpleName + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + BackupCreationFragment.newInstance(walletAddress, password)) + .commit() + } + + override fun askForWritePermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || + ActivityCompat.checkSelfPermission(this, + Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + onPermissionSubject?.onNext(Unit) + } else { + requestStorageWritePermission() + } + } + + override fun showSuccessScreen() { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, BackupSuccessFragment.newInstance()) + .commit() + } + + override fun closeScreen() = finish() + + override fun onPermissionGiven() = onPermissionSubject!! + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + when (presenter.currentFragmentName) { + BackupCreationFragment::class.java.simpleName -> walletsEventSender.sendWalletSaveFileEvent( + WalletsAnalytics.ACTION_BACK, WalletsAnalytics.STATUS_FAIL, + WalletsAnalytics.REASON_CANCELED) + } + } + } + return super.onOptionsItemSelected(item) + } + + override fun openSystemFileDirectory(fileName: String) { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { + putExtra(FILE_NAME_EXTRA_KEY, fileName) + } + try { + startActivityForResult(intent, ACTION_OPEN_DOCUMENT_TREE_REQUEST_CODE) + } catch (ex: ActivityNotFoundException) { + Snackbar.make(backup_content, R.string.unknown_error, Snackbar.LENGTH_SHORT) + .show() + } + } + + private fun requestStorageWritePermission() = + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + RC_WRITE_EXTERNAL_STORAGE_PERMISSION) + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, + grantResults: IntArray) { + if (requestCode == RC_WRITE_EXTERNAL_STORAGE_PERMISSION) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + onPermissionSubject?.onNext(Unit) + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == ACTION_OPEN_DOCUMENT_TREE_REQUEST_CODE) { + var systemFileIntentResult = SystemFileIntentResult() + if (resultCode == RESULT_OK && data != null) { + data.data?.let { + val documentFile = DocumentFile.fromTreeUri(this, it) + systemFileIntentResult = SystemFileIntentResult(documentFile) + } + } + onDocumentFileSubject?.onNext(systemFileIntentResult) + } + } + + override fun onSystemFileIntentResult() = onDocumentFileSubject!! + + override fun setupToolbar() { + toolbar() + } + + override fun onDestroy() { + onPermissionSubject = null + onDocumentFileSubject = null + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/AppcoinsBalanceRepository.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/AppcoinsBalanceRepository.kt new file mode 100644 index 00000000000..6da1be8b535 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/AppcoinsBalanceRepository.kt @@ -0,0 +1,117 @@ +package com.asfoundation.wallet.ui.balance + +import android.util.Pair +import com.asfoundation.wallet.entity.Balance +import com.asfoundation.wallet.interact.GetDefaultWalletBalanceInteract +import com.asfoundation.wallet.service.LocalCurrencyConversionService +import com.asfoundation.wallet.ui.balance.database.BalanceDetailsDao +import com.asfoundation.wallet.ui.balance.database.BalanceDetailsEntity +import com.asfoundation.wallet.ui.balance.database.BalanceDetailsMapper +import com.asfoundation.wallet.ui.iab.FiatValue +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.Disposable + +class AppcoinsBalanceRepository( + private val balanceGetter: GetDefaultWalletBalanceInteract, + private val localCurrencyConversionService: LocalCurrencyConversionService, + private val balanceDetailsDao: BalanceDetailsDao, + private val balanceDetailsMapper: BalanceDetailsMapper, + private val networkScheduler: Scheduler) : + BalanceRepository { + private var ethBalanceDisposable: Disposable? = null + private var appcBalanceDisposable: Disposable? = null + private var creditsBalanceDisposable: Disposable? = null + + companion object { + private const val SUM_FIAT_SCALE = 4 + } + + override fun getEthBalance(address: String): Observable> { + if (ethBalanceDisposable == null || ethBalanceDisposable!!.isDisposed) { + ethBalanceDisposable = balanceGetter.getEthereumBalance(address) + .observeOn(networkScheduler) + .flatMapObservable { balance -> + localCurrencyConversionService.getEtherToLocalFiat(balance.getStringValue(), + SUM_FIAT_SCALE) + .map { fiatValue -> + balanceDetailsDao.updateEthBalance(address, balance.getStringValue(), + fiatValue.amount.toString(), fiatValue.currency, fiatValue.symbol) + } + } + .subscribe({}, { it.printStackTrace() }) + } + return getBalance(address) + .map { balanceDetailsMapper.mapEthBalance(it) } + } + + override fun getAppcBalance(address: String): Observable> { + if (appcBalanceDisposable == null || appcBalanceDisposable!!.isDisposed) { + balanceGetter.getAppcBalance(address) + .observeOn(networkScheduler) + .flatMapObservable { balance -> + localCurrencyConversionService.getAppcToLocalFiat(balance.getStringValue(), + SUM_FIAT_SCALE) + .map { fiatValue -> + balanceDetailsDao.updateAppcBalance(address, balance.getStringValue(), + fiatValue.amount.toString(), fiatValue.currency, fiatValue.symbol) + } + } + .onExceptionResumeNext {} + .subscribe() + } + return getBalance(address) + .map { balanceDetailsMapper.mapAppcBalance(it) } + } + + override fun getCreditsBalance(address: String): Observable> { + if (creditsBalanceDisposable == null || creditsBalanceDisposable!!.isDisposed) { + balanceGetter.getCredits(address) + .observeOn(networkScheduler) + .flatMapObservable { balance -> + localCurrencyConversionService.getAppcToLocalFiat(balance.getStringValue(), + SUM_FIAT_SCALE) + .map { fiatValue -> + balanceDetailsDao.updateCreditsBalance(address, balance.getStringValue(), + fiatValue.amount.toString(), fiatValue.currency, fiatValue.symbol) + } + } + .onExceptionResumeNext {} + .subscribe() + } + return getBalance(address) + .map { balanceDetailsMapper.mapCreditsBalance(it) } + } + + private fun getBalance(walletAddress: String): Observable { + checkIfExistsOrCreate(walletAddress) + return balanceDetailsDao.getBalance(walletAddress) + } + + @Synchronized + private fun checkIfExistsOrCreate(walletAddress: String) { + val entity = balanceDetailsDao.getSyncBalance(walletAddress) + if (entity == null) { + balanceDetailsDao.insert(balanceDetailsMapper.map(walletAddress)) + } + } + + override fun getStoredEthBalance(walletAddress: String): Single> { + return getBalance(walletAddress) + .map { balanceDetailsMapper.mapEthBalance(it) } + .firstOrError() + } + + override fun getStoredAppcBalance(walletAddress: String): Single> { + return getBalance(walletAddress) + .map { balanceDetailsMapper.mapAppcBalance(it) } + .firstOrError() + } + + override fun getStoredCreditsBalance(walletAddress: String): Single> { + return getBalance(walletAddress) + .map { balanceDetailsMapper.mapCreditsBalance(it) } + .firstOrError() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceActivity.kt new file mode 100644 index 00000000000..0a6cce694b4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceActivity.kt @@ -0,0 +1,169 @@ +package com.asfoundation.wallet.ui.balance + +import android.animation.Animator +import android.app.Activity +import android.content.Intent +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.transition.Fade +import android.view.MenuItem +import android.view.View +import android.view.Window +import android.widget.ImageView +import android.widget.TextView +import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.ViewCompat +import com.asf.wallet.R +import com.asfoundation.wallet.router.TransactionsRouter +import com.asfoundation.wallet.ui.BaseActivity +import com.asfoundation.wallet.ui.backup.WalletBackupActivity.Companion.newIntent +import com.asfoundation.wallet.ui.wallets.RemoveWalletActivity +import com.asfoundation.wallet.ui.wallets.WalletDetailsFragment +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.activity_balance.* +import kotlinx.android.synthetic.main.remove_wallet_activity_layout.* + + +class BalanceActivity : BaseActivity(), + BalanceActivityView { + + private lateinit var activityPresenter: BalanceActivityPresenter + private var onBackPressedSubject: PublishSubject? = null + private var backEnabled = true + private var expandBottomSheet: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) + val fade = Fade() + fade.duration = 500 + window.enterTransition = fade + + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_balance) + onBackPressedSubject = PublishSubject.create() + activityPresenter = BalanceActivityPresenter(this) + activityPresenter.present(savedInstanceState) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + if (wallet_remove_animation == null || wallet_creation_animation.visibility != View.VISIBLE) { + if (backEnabled) { + super.onBackPressed() + } else { + onBackPressedSubject?.onNext("") + } + } + return true + } + return super.onOptionsItemSelected(item) + } + + override fun onBackPressed() { + if (wallet_remove_animation == null || wallet_remove_animation.visibility != View.VISIBLE) super.onBackPressed() + } + + override fun showBalanceScreen() { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, BalanceFragment.newInstance()) + .commit() + } + + override fun showTokenDetailsScreen( + tokenDetailsId: TokenDetailsActivity.TokenDetailsId, imgView: ImageView, + textView: TextView, parentView: View) { + + val intent = TokenDetailsActivity.newInstance(this, tokenDetailsId) + + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, + androidx.core.util.Pair(imgView, ViewCompat.getTransitionName(imgView)!!), + androidx.core.util.Pair(textView, ViewCompat.getTransitionName(textView)!!), + androidx.core.util.Pair(parentView, + ViewCompat.getTransitionName(parentView)!!)) + + startActivity(intent, options.toBundle()) + + } + + override fun navigateToWalletDetailView(walletAddress: String, isActive: Boolean) { + expandBottomSheet = true + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + WalletDetailsFragment.newInstance(walletAddress, isActive)) + .addToBackStack(WalletDetailsFragment::class.java.simpleName) + .commit() + } + + override fun navigateToRemoveWalletView(walletAddress: String, totalFiatBalance: String, + appcoinsBalance: String, creditsBalance: String, + ethereumBalance: String) { + startActivityForResult( + RemoveWalletActivity.newIntent(this, walletAddress, totalFiatBalance, appcoinsBalance, + creditsBalance, ethereumBalance), REQUEST_CODE) + } + + override fun navigateToBackupView(walletAddress: String) { + startActivity(newIntent(this, walletAddress)) + } + + override fun navigateToRestoreView() { + startActivity(RestoreWalletActivity.newIntent(this)) + } + + override fun showCreatingAnimation() { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED + wallet_creation_animation.visibility = View.VISIBLE + create_wallet_animation.playAnimation() + } + + override fun showWalletCreatedAnimation() { + create_wallet_animation.setAnimation(R.raw.success_animation) + create_wallet_text.text = getText(R.string.provide_wallet_created_header) + create_wallet_animation.addAnimatorListener(object : Animator.AnimatorListener { + override fun onAnimationRepeat(animation: Animator?) = Unit + override fun onAnimationEnd(animation: Animator?) = navigateToTransactions() + override fun onAnimationCancel(animation: Animator?) = Unit + override fun onAnimationStart(animation: Animator?) = Unit + }) + create_wallet_animation.repeatCount = 0 + create_wallet_animation.playAnimation() + } + + override fun shouldExpandBottomSheet(): Boolean { + val shouldExpand = expandBottomSheet + expandBottomSheet = false + return shouldExpand + } + + override fun setupToolbar() { + toolbar() + } + + override fun enableBack() { + backEnabled = true + } + + override fun disableBack() { + backEnabled = false + } + + override fun backPressed() = onBackPressedSubject!! + + + override fun navigateToTransactions() { + TransactionsRouter().open(this, true) + finish() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) { + expandBottomSheet = true + showBalanceScreen() + } + } + + companion object { + private const val REQUEST_CODE = 123 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceActivityPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceActivityPresenter.kt new file mode 100644 index 00000000000..fa3df904a8c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceActivityPresenter.kt @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.ui.balance + +import android.os.Bundle + +class BalanceActivityPresenter(private val view: BalanceActivityView) { + + fun present(savedInstanceState: Bundle?) { + savedInstanceState ?: view.showBalanceScreen() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceActivityView.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceActivityView.kt new file mode 100644 index 00000000000..182f4998b81 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceActivityView.kt @@ -0,0 +1,41 @@ +package com.asfoundation.wallet.ui.balance + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import io.reactivex.Observable + +interface BalanceActivityView { + + fun showBalanceScreen() + + fun showTokenDetailsScreen( + tokenDetailsId: TokenDetailsActivity.TokenDetailsId, imgView: ImageView, + textView: TextView, parentView: View) + + fun setupToolbar() + + fun navigateToWalletDetailView(walletAddress: String, isActive: Boolean) + + fun shouldExpandBottomSheet(): Boolean + + fun enableBack() + + fun disableBack() + + fun backPressed(): Observable + + fun navigateToTransactions() + + fun navigateToRemoveWalletView(walletAddress: String, totalFiatBalance: String, + appcoinsBalance: String, creditsBalance: String, + ethereumBalance: String) + + fun navigateToBackupView(walletAddress: String) + + fun navigateToRestoreView() + + fun showCreatingAnimation() + + fun showWalletCreatedAnimation() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceFragment.kt new file mode 100644 index 00000000000..5aa10fd4332 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceFragment.kt @@ -0,0 +1,366 @@ +package com.asfoundation.wallet.ui.balance + +import android.annotation.SuppressLint +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.res.Resources +import android.os.Bundle +import android.util.TypedValue +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.PopupWindow +import com.airbnb.lottie.LottieAnimationView +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.ui.MyAddressActivity +import com.asfoundation.wallet.ui.wallets.WalletsFragment +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.asfoundation.wallet.wallet_validation.generic.WalletValidationActivity +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.snackbar.Snackbar +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.backup_tooltip.view.* +import kotlinx.android.synthetic.main.balance_token_item.view.* +import kotlinx.android.synthetic.main.fragment_balance.* +import kotlinx.android.synthetic.main.fragment_balance.bottom_sheet_fragment_container +import kotlinx.android.synthetic.main.invite_friends_fragment_layout.* +import kotlinx.android.synthetic.main.unverified_layout.* +import javax.inject.Inject +import kotlin.math.abs + +class BalanceFragment : BasePageViewFragment(), BalanceFragmentView { + + @Inject + lateinit var balanceInteract: BalanceInteract + + @Inject + lateinit var walletsEventSender: WalletsEventSender + + @Inject + lateinit var formatter: CurrencyFormatUtils + + private var onBackPressedSubject: PublishSubject? = null + private var activityView: BalanceActivityView? = null + private var showingAnimation: Boolean = false + private var popup: PopupWindow? = null + private lateinit var tooltip: View + private lateinit var walletsBottomSheet: BottomSheetBehavior + private lateinit var presenter: BalanceFragmentPresenter + + companion object { + @JvmStatic + fun newInstance() = BalanceFragment() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is BalanceActivityView) { + throw IllegalStateException("Balance Fragment must be attached to Balance Activity") + } + activityView = context + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = BalanceFragmentPresenter(this, activityView, balanceInteract, walletsEventSender, + Schedulers.io(), AndroidSchedulers.mainThread(), CompositeDisposable(), formatter) + onBackPressedSubject = PublishSubject.create() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + tooltip = layoutInflater.inflate(R.layout.backup_tooltip, null) + childFragmentManager.beginTransaction() + .replace(R.id.bottom_sheet_fragment_container, WalletsFragment()) + .commit() + return inflater.inflate(R.layout.fragment_balance, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + walletsBottomSheet = + BottomSheetBehavior.from(bottom_sheet_fragment_container) + setBackListener(view) + activityView?.let { + if (it.shouldExpandBottomSheet()) walletsBottomSheet.state = + BottomSheetBehavior.STATE_EXPANDED + } + animateBackgroundFade() + activityView?.setupToolbar() + presenter.present() + + (app_bar as AppBarLayout).addOnOffsetChangedListener( + AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> + if (balance_label != null) { + val percentage = abs(verticalOffset).toFloat() / appBarLayout.totalScrollRange + setAlpha(balance_label, percentage) + setAlpha(balance_value, percentage) + setAlpha(balance_label_placeholder, percentage) + setAlpha(balance_value_placeholder, percentage) + } + }) + } + + override fun onResume() { + super.onResume() + presenter.onResume() + } + + override fun setTooltip() { + popup = PopupWindow(tooltip) + popup?.height = ViewGroup.LayoutParams.WRAP_CONTENT + popup?.width = ViewGroup.LayoutParams.MATCH_PARENT + val offset = dpToPx(25f) + faded_background.visibility = View.VISIBLE + popup?.showAsDropDown(backup_active_button, 0, offset * -1) + } + + override fun onDestroyView() { + activityView?.enableBack() + presenter.stop() + super.onDestroyView() + } + + override fun setupUI() { + balance_value_placeholder.playAnimation() + balance_label_placeholder.playAnimation() + + appcoins_credits_token.token_icon.setImageResource(R.drawable.ic_appc_c_token) + appcoins_credits_token.token_name.text = getString(R.string.appc_credits_token_name) + (appcoins_credits_token.token_balance_placeholder as LottieAnimationView).playAnimation() + + appcoins_token.token_icon.setImageResource(R.drawable.ic_appc_token) + appcoins_token.token_name.text = getString(R.string.appc_token_name) + (appcoins_token.token_balance_placeholder as LottieAnimationView).playAnimation() + + ether_token.token_icon.setImageResource(R.drawable.ic_eth_token) + ether_token.token_name.text = getString(R.string.ethereum_token_name) + (ether_token.token_balance_placeholder as LottieAnimationView).playAnimation() + } + + @SuppressLint("SetTextI18n") + override fun updateTokenValue(tokenBalance: String, + fiatBalance: String, + tokenCurrency: WalletCurrency, + fiatCurrency: String) { + if (tokenBalance != "-1" && fiatBalance != "-1") { + when (tokenCurrency) { + WalletCurrency.CREDITS -> { + appcoins_credits_token.token_balance_placeholder.visibility = View.GONE + (appcoins_credits_token.token_balance_placeholder as LottieAnimationView).cancelAnimation() + appcoins_credits_token.token_balance.text = + "$tokenBalance ${tokenCurrency.symbol}" + appcoins_credits_token.token_balance.visibility = View.VISIBLE + appcoins_credits_token.token_balance_converted.text = + "$fiatCurrency$fiatBalance" + appcoins_credits_token.token_balance_converted.visibility = View.VISIBLE + } + WalletCurrency.APPCOINS -> { + appcoins_token.token_balance_placeholder.visibility = View.GONE + (appcoins_token.token_balance_placeholder as LottieAnimationView).cancelAnimation() + appcoins_token.token_balance.text = + "$tokenBalance ${tokenCurrency.symbol}" + appcoins_token.token_balance.visibility = View.VISIBLE + appcoins_token.token_balance_converted.text = + "$fiatCurrency$fiatBalance" + appcoins_token.token_balance_converted.visibility = View.VISIBLE + } + WalletCurrency.ETHEREUM -> { + ether_token.token_balance_placeholder.visibility = View.GONE + (ether_token.token_balance_placeholder as LottieAnimationView).cancelAnimation() + ether_token.token_balance.text = + "$tokenBalance ${tokenCurrency.symbol}" + ether_token.token_balance.visibility = View.VISIBLE + ether_token.token_balance_converted.text = + "$fiatCurrency$fiatBalance" + ether_token.token_balance_converted.visibility = View.VISIBLE + } + else -> return + } + } + } + + @SuppressLint("SetTextI18n") + override fun updateOverallBalance(overallBalance: String, currency: String, symbol: String) { + if (overallBalance != "-1") { + balance_label_placeholder.visibility = View.GONE + (balance_label_placeholder as LottieAnimationView).cancelAnimation() + balance_label.text = + String.format(getString(R.string.balance_total_body), currency) + balance_label.visibility = View.VISIBLE + + balance_value_placeholder.visibility = View.GONE + (balance_value_placeholder as LottieAnimationView).cancelAnimation() + balance_value.text = symbol + overallBalance + balance_value.visibility = View.VISIBLE + } + } + + override fun getCreditClick(): Observable = RxView.clicks(appcoins_credits_token) + .map { appcoins_credits_token } + + override fun getAppcClick(): Observable = RxView.clicks(appcoins_token) + .map { appcoins_token } + + override fun getEthClick(): Observable = RxView.clicks(ether_token) + .map { ether_token } + + override fun showTokenDetails(view: View) { + lateinit var tokenId: TokenDetailsActivity.TokenDetailsId + when (view) { + appcoins_credits_token -> tokenId = TokenDetailsActivity.TokenDetailsId.APPC_CREDITS + appcoins_token -> tokenId = TokenDetailsActivity.TokenDetailsId.APPC + ether_token -> tokenId = TokenDetailsActivity.TokenDetailsId.ETHER + } + + activityView?.showTokenDetailsScreen(tokenId, view.token_icon, view.token_name, view) + } + + override fun getVerifyWalletClick() = RxView.clicks(verify_wallet_button) + + override fun getCopyClick() = RxView.clicks(copy_address) + + override fun getQrCodeClick() = RxView.clicks(wallet_qr_code) + + override fun getBackupClick() = RxView.clicks(backup_active_button) + + override fun getTooltipDismissClick() = RxView.clicks(tooltip.tooltip_later_button) + + override fun getTooltipBackupButton() = RxView.clicks(tooltip.tooltip_backup_button) + + override fun setWalletAddress(walletAddress: String) { + active_wallet_address.text = walletAddress + } + + override fun setAddressToClipBoard(walletAddress: String) { + val clipboard = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? + val clip = ClipData.newPlainText(MyAddressActivity.KEY_ADDRESS, walletAddress) + clipboard?.setPrimaryClip(clip) + + view?.let { + Snackbar.make(it, R.string.wallets_address_copied_body, Snackbar.LENGTH_SHORT) + .show() + } + } + + override fun showQrCodeView() { + context?.let { startActivityForResult(QrCodeActivity.newIntent(it), 12) } + } + + override fun backPressed() = onBackPressedSubject!! + + override fun homeBackPressed() = activityView?.backPressed() + + override fun handleBackPress() { + if (walletsBottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) { + walletsBottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + } else { + activityView?.navigateToTransactions() + } + } + + override fun openWalletValidationScreen() { + context?.let { + val intent = WalletValidationActivity.newIntent(it, hasBeenInvitedFlow = false, + navigateToTransactionsOnSuccess = false, navigateToTransactionsOnCancel = false, + showToolbar = true, previousContext = "settings") + .apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } + startActivity(intent) + } + } + + override fun showVerifiedWalletChip() { + verified_wallet_chip.visibility = View.VISIBLE + } + + override fun hideVerifiedWalletChip() { + verified_wallet_chip.visibility = View.GONE + } + + override fun showUnverifiedWalletChip() { + unverified_wallet_layout.visibility = View.VISIBLE + } + + override fun hideUnverifiedWalletChip() { + unverified_wallet_layout.visibility = View.GONE + } + + override fun disableVerifyWalletButton() { + verify_wallet_button.isEnabled = false + } + + override fun enableVerifyWalletButton() { + verify_wallet_button.isEnabled = true + } + + override fun showCreatingAnimation() { + showingAnimation = true + activityView?.showCreatingAnimation() + } + + override fun showWalletCreatedAnimation() { + showingAnimation = false + activityView?.showWalletCreatedAnimation() + } + + override fun changeBottomSheetState() { + if (walletsBottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) { + walletsBottomSheet.state = BottomSheetBehavior.STATE_EXPANDED + } else if (walletsBottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) { + walletsBottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + } + } + + override fun dismissTooltip() { + faded_background.visibility = View.GONE + popup?.dismiss() + } + + private fun setBackListener(view: View) { + activityView?.disableBack() + view.apply { + isFocusableInTouchMode = true + requestFocus() + setOnKeyListener { _, keyCode, keyEvent -> + if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) { + if (popup != null && popup?.isShowing == true) { + dismissTooltip() + presenter.saveSeenToolTip() + } else if (!showingAnimation) onBackPressedSubject?.onNext("") + } + true + } + } + } + + private fun animateBackgroundFade() { + walletsBottomSheet.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) = Unit + override fun onSlide(bottomSheet: View, slideOffset: Float) { + background_fade_animation?.progress = slideOffset + } + }) + } + + private fun setAlpha(view: View, alphaPercentage: Float) { + view.alpha = 1 - alphaPercentage * 1.20f + } + + private fun dpToPx(value: Float) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, + Resources.getSystem().displayMetrics) + .toInt() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceFragmentPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceFragmentPresenter.kt new file mode 100644 index 00000000000..0bf2e4d5362 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceFragmentPresenter.kt @@ -0,0 +1,237 @@ +package com.asfoundation.wallet.ui.balance + +import com.asfoundation.wallet.billing.analytics.WalletsAnalytics +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import java.math.BigDecimal +import java.util.concurrent.TimeUnit + +class BalanceFragmentPresenter(private val view: BalanceFragmentView, + private val activityView: BalanceActivityView?, + private val balanceInteract: BalanceInteract, + private val walletsEventSender: WalletsEventSender, + private val networkScheduler: Scheduler, + private val viewScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val formatter: CurrencyFormatUtils) { + + + companion object { + const val APPC_CURRENCY = "APPC_CURRENCY" + const val APPC_C_CURRENCY = "APPC_C_CURRENCY" + const val ETH_CURRENCY = "ETH_CURRENCY" + val BIG_DECIMAL_MINUS_ONE = BigDecimal("-1") + } + + fun present() { + view.setupUI() + requestBalances() + handleTokenDetailsClick() + handleCopyClick() + handleQrCodeClick() + handleBackupClick() + handleBackPress() + handleSetupTooltip() + handleTooltipBackupClick() + handleTooltipLaterClick() + handleVerifyWalletClick() + handleCachedWalletInfoDisplay() + } + + fun onResume() { + handleWalletInfoDisplay() + } + + private fun handleTooltipLaterClick() { + disposables.add(view.getTooltipDismissClick() + .doOnNext { + view.dismissTooltip() + balanceInteract.saveSeenBackupTooltip() + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleTooltipBackupClick() { + disposables.add(view.getTooltipBackupButton() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .flatMapSingle { balanceInteract.requestActiveWalletAddress() } + .observeOn(viewScheduler) + .doOnNext { + balanceInteract.saveSeenBackupTooltip() + activityView?.navigateToBackupView(it) + view.dismissTooltip() + } + .doOnNext { + walletsEventSender.sendCreateBackupEvent(WalletsAnalytics.ACTION_CREATE, + WalletsAnalytics.CONTEXT_WALLET_TOOLTIP, WalletsAnalytics.STATUS_SUCCESS) + } + .doOnError { + walletsEventSender.sendCreateBackupEvent(WalletsAnalytics.ACTION_CREATE, + WalletsAnalytics.CONTEXT_WALLET_TOOLTIP, WalletsAnalytics.STATUS_FAIL, it.message) + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleSetupTooltip() { + disposables.add(Single.just(balanceInteract.hasSeenBackupTooltip()) + .subscribeOn(networkScheduler) + .filter { !it } + .observeOn(viewScheduler) + .doOnSuccess { view.setTooltip() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleBackPress() { + disposables.add(Observable.merge(view.backPressed(), view.homeBackPressed()) + .observeOn(viewScheduler) + .doOnNext { view.handleBackPress() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleVerifyWalletClick() { + disposables.add(view.getVerifyWalletClick() + .observeOn(viewScheduler) + .doOnNext { view.openWalletValidationScreen() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleCachedWalletInfoDisplay() { + disposables.add(balanceInteract.requestActiveWalletAddress() + .observeOn(viewScheduler) + .doOnSuccess { handleValidationCache(it) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleWalletInfoDisplay() { + disposables.add(balanceInteract.requestActiveWalletAddress() + .observeOn(viewScheduler) + .doOnSuccess { view.setWalletAddress(it) } + .observeOn(networkScheduler) + .flatMap { balanceInteract.isWalletValid(it) } + .observeOn(viewScheduler) + .doOnSuccess { + when (it.second) { + WalletValidationStatus.SUCCESS -> displayWalletVerifiedStatus() + WalletValidationStatus.GENERIC_ERROR -> displayWalletUnverifiedStatus() + WalletValidationStatus.NO_NETWORK -> handleNoNetwork(it.first) + else -> handleValidationCache(it.first) + } + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun requestBalances() { + disposables.add(balanceInteract.requestTokenConversion() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnNext { updateUI(it) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun updateUI(balanceScreenModel: BalanceScreenModel) { + updateTokenBalance(balanceScreenModel.appcBalance, WalletCurrency.APPCOINS) + updateTokenBalance(balanceScreenModel.creditsBalance, WalletCurrency.CREDITS) + updateTokenBalance(balanceScreenModel.ethBalance, WalletCurrency.ETHEREUM) + updateOverallBalance(balanceScreenModel.overallFiat) + } + + private fun handleTokenDetailsClick() { + disposables.add( + Observable.merge(view.getCreditClick(), view.getAppcClick(), view.getEthClick()) + .throttleFirst(500, TimeUnit.MILLISECONDS) + .map { view.showTokenDetails(it) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun updateTokenBalance(balance: TokenBalance, currency: WalletCurrency) { + var tokenBalance = "-1" + var fiatBalance = "-1" + if (balance.token.amount.compareTo(BigDecimal("-1")) == 1) { + tokenBalance = formatter.formatCurrency(balance.token.amount, currency) + fiatBalance = formatter.formatCurrency(balance.fiat.amount) + } + view.updateTokenValue(tokenBalance, fiatBalance, currency, balance.fiat.symbol) + } + + private fun updateOverallBalance(balance: FiatValue) { + var overallBalance = "-1" + if (balance.amount.compareTo(BigDecimal("-1")) == 1) { + overallBalance = formatter.formatCurrency(balance.amount) + } + view.updateOverallBalance(overallBalance, balance.currency, balance.symbol) + } + + private fun handleCopyClick() { + disposables.add(view.getCopyClick() + .flatMapSingle { balanceInteract.requestActiveWalletAddress() } + .observeOn(viewScheduler) + .doOnNext { view.setAddressToClipBoard(it) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleQrCodeClick() { + disposables.add(view.getQrCodeClick() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .doOnNext { view.showQrCodeView() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleBackupClick() { + disposables.add(view.getBackupClick() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .flatMapSingle { balanceInteract.requestActiveWalletAddress() } + .observeOn(viewScheduler) + .doOnNext { activityView?.navigateToBackupView(it) } + .doOnNext { + walletsEventSender.sendCreateBackupEvent(WalletsAnalytics.ACTION_CREATE, + WalletsAnalytics.CONTEXT_WALLET_BALANCE, WalletsAnalytics.STATUS_SUCCESS) + } + .doOnError { + walletsEventSender.sendCreateBackupEvent(WalletsAnalytics.ACTION_CREATE, + WalletsAnalytics.CONTEXT_WALLET_BALANCE, WalletsAnalytics.STATUS_FAIL) + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleValidationCache(address: String) { + if (balanceInteract.isWalletValidated(address)) displayWalletVerifiedStatus() + else displayWalletUnverifiedStatus() + } + + private fun handleNoNetwork(address: String) { + if (balanceInteract.isWalletValidated(address)) displayWalletVerifiedStatus() + else displayNoNetworkStatus() + } + + private fun displayWalletVerifiedStatus() { + view.showVerifiedWalletChip() + view.hideUnverifiedWalletChip() + } + + private fun displayWalletUnverifiedStatus() { + view.showUnverifiedWalletChip() + view.hideVerifiedWalletChip() + view.enableVerifyWalletButton() + } + + private fun displayNoNetworkStatus() { + view.showUnverifiedWalletChip() + view.hideVerifiedWalletChip() + view.disableVerifyWalletButton() + } + + fun saveSeenToolTip() { + balanceInteract.saveSeenBackupTooltip() + } + + fun stop() = disposables.clear() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceFragmentView.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceFragmentView.kt new file mode 100644 index 00000000000..ca96292d7d9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceFragmentView.kt @@ -0,0 +1,73 @@ +package com.asfoundation.wallet.ui.balance + +import android.view.View +import com.asfoundation.wallet.util.WalletCurrency +import io.reactivex.Observable + +interface BalanceFragmentView { + + fun setupUI() + + fun updateTokenValue(tokenBalance: String, + fiatBalance: String, + tokenCurrency: WalletCurrency, + fiatCurrency: String) + + fun updateOverallBalance(overallBalance: String, currency: String, symbol: String) + + fun getCreditClick(): Observable + + fun getAppcClick(): Observable + + fun getEthClick(): Observable + + fun showTokenDetails(view: View) + + fun getCopyClick(): Observable + + fun getQrCodeClick(): Observable + + fun setWalletAddress(walletAddress: String) + + fun setAddressToClipBoard(walletAddress: String) + + fun showQrCodeView() + + fun backPressed(): Observable + + fun handleBackPress() + + fun showWalletCreatedAnimation() + + fun showCreatingAnimation() + + fun changeBottomSheetState() + + fun getBackupClick(): Observable + + fun setTooltip() + + fun getTooltipDismissClick(): Observable + + fun getTooltipBackupButton(): Observable + + fun homeBackPressed(): Observable? + + fun dismissTooltip() + + fun getVerifyWalletClick(): Observable + + fun openWalletValidationScreen() + + fun showVerifiedWalletChip() + + fun hideVerifiedWalletChip() + + fun showUnverifiedWalletChip() + + fun hideUnverifiedWalletChip() + + fun disableVerifyWalletButton() + + fun enableVerifyWalletButton() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceInteract.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceInteract.kt new file mode 100644 index 00000000000..e682e03f3d2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceInteract.kt @@ -0,0 +1,167 @@ +package com.asfoundation.wallet.ui.balance + +import android.util.Pair +import com.asfoundation.wallet.entity.Balance +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.ui.TokenValue +import com.asfoundation.wallet.ui.balance.BalanceFragmentPresenter.Companion.APPC_CURRENCY +import com.asfoundation.wallet.ui.balance.BalanceFragmentPresenter.Companion.APPC_C_CURRENCY +import com.asfoundation.wallet.ui.balance.BalanceFragmentPresenter.Companion.ETH_CURRENCY +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.annotations.Nullable +import io.reactivex.functions.Function3 +import java.math.BigDecimal + +class BalanceInteract( + private val walletInteract: FindDefaultWalletInteract, + private val balanceRepository: BalanceRepository, + private val preferencesRepositoryType: PreferencesRepositoryType, + private val smsValidationInteract: SmsValidationInteract) { + + fun getAppcBalance(): Observable> { + return walletInteract.find() + .flatMapObservable { balanceRepository.getAppcBalance(it.address) } + } + + fun getEthBalance(): Observable> { + return walletInteract.find() + .flatMapObservable { balanceRepository.getEthBalance(it.address) } + } + + fun getCreditsBalance(): Observable> { + return walletInteract.find() + .flatMapObservable { balanceRepository.getCreditsBalance(it.address) } + } + + private fun getStoredAppcBalance(walletAddress: String?): Single> { + return (walletAddress?.let { Single.just(it) } ?: walletInteract.find() + .map { it.address }) + .flatMap { balanceRepository.getStoredAppcBalance(it) } + } + + private fun getStoredEthBalance(walletAddress: String?): Single> { + return (walletAddress?.let { Single.just(it) } ?: walletInteract.find() + .map { it.address }) + .flatMap { balanceRepository.getStoredEthBalance(it) } + } + + private fun getStoredCreditsBalance(walletAddress: String?): Single> { + return (walletAddress?.let { Single.just(it) } ?: walletInteract.find() + .map { it.address }) + .flatMap { balanceRepository.getStoredCreditsBalance(it) } + } + + fun requestTokenConversion(): Observable { + return Observable.zip( + getCreditsBalance(), + getAppcBalance(), + getEthBalance(), + Function3 { creditsBalance, appcBalance, ethBalance -> + mapToBalanceScreenModel(creditsBalance, appcBalance, ethBalance) + } + ) + } + + fun getTotalBalance(address: String): Observable { + return Observable.zip( + balanceRepository.getCreditsBalance(address), + balanceRepository.getAppcBalance(address), + balanceRepository.getEthBalance(address), + Function3 { creditsBalance, appcBalance, ethBalance -> + getOverallBalance(mapToBalance(creditsBalance, APPC_C_CURRENCY), + mapToBalance(appcBalance, APPC_CURRENCY), mapToBalance(ethBalance, ETH_CURRENCY)) + }) + } + + fun requestActiveWalletAddress(): Single { + return walletInteract.find() + .map { it.address } + } + + fun getStoredOverallBalance(@Nullable walletAddress: String? = null): Single { + return Single.zip( + getStoredAppcBalance(walletAddress), + getStoredEthBalance(walletAddress), + getStoredCreditsBalance(walletAddress), + Function3 { creditsBalance, appcBalance, ethBalance -> + mapOverallBalance(creditsBalance, appcBalance, ethBalance) + } + ) + } + + fun getStoredBalanceScreenModel(walletAddress: String): Single { + return Single.zip( + getStoredAppcBalance(walletAddress), + getStoredEthBalance(walletAddress), + getStoredCreditsBalance(walletAddress), + Function3 { appcBalance, ethBalance, creditsBalance -> + mapToBalanceScreenModel(creditsBalance, appcBalance, ethBalance) + } + ) + } + + fun isWalletValid(address: String): Single> { + return smsValidationInteract.getValidationStatus(address) + .map { Pair(address, it) } + } + + fun isWalletValidated(address: String) = preferencesRepositoryType.isWalletValidated(address) + + fun hasSeenBackupTooltip() = preferencesRepositoryType.getSeenBackupTooltip() + + fun saveSeenBackupTooltip() = preferencesRepositoryType.saveSeenBackupTooltip() + + private fun mapOverallBalance(creditsBalance: Pair, + appcBalance: Pair, + ethBalance: Pair): FiatValue { + var balance = getAddBalanceValue(BalanceFragmentPresenter.BIG_DECIMAL_MINUS_ONE, + creditsBalance.second.amount) + balance = getAddBalanceValue(balance, appcBalance.second.amount) + balance = getAddBalanceValue(balance, ethBalance.second.amount) + + return FiatValue(balance, appcBalance.second.currency, appcBalance.second.symbol) + + } + + private fun mapToBalanceScreenModel(creditsBalance: Pair, + appcBalance: Pair, + ethBalance: Pair): BalanceScreenModel { + val credits = mapToBalance(creditsBalance, APPC_C_CURRENCY) + val appc = mapToBalance(appcBalance, APPC_CURRENCY) + val eth = mapToBalance(ethBalance, ETH_CURRENCY) + val overall = getOverallBalance(credits, appc, eth) + return BalanceScreenModel(overall, credits, appc, eth) + } + + private fun mapToBalance(pair: Pair, currency: String): TokenBalance { + return TokenBalance(TokenValue(pair.first.value, currency, pair.first.symbol), pair.second) + } + + private fun getOverallBalance(creditsBalance: TokenBalance, + appcBalance: TokenBalance, + ethBalance: TokenBalance): FiatValue { + var balance = getAddBalanceValue(BalanceFragmentPresenter.BIG_DECIMAL_MINUS_ONE, + creditsBalance.fiat.amount) + balance = getAddBalanceValue(balance, appcBalance.fiat.amount) + balance = getAddBalanceValue(balance, ethBalance.fiat.amount) + return FiatValue(balance, appcBalance.fiat.currency, appcBalance.fiat.symbol) + } + + private fun getAddBalanceValue(currentValue: BigDecimal, value: BigDecimal): BigDecimal { + return if (value.compareTo(BalanceFragmentPresenter.BIG_DECIMAL_MINUS_ONE) == 1) { + if (currentValue.compareTo(BalanceFragmentPresenter.BIG_DECIMAL_MINUS_ONE) == 1) { + currentValue.add(value) + } else { + value + } + } else { + currentValue + } + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceRepository.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceRepository.kt new file mode 100644 index 00000000000..aee1851325f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceRepository.kt @@ -0,0 +1,21 @@ +package com.asfoundation.wallet.ui.balance + +import android.util.Pair +import com.asfoundation.wallet.entity.Balance +import com.asfoundation.wallet.ui.iab.FiatValue +import io.reactivex.Observable +import io.reactivex.Single + +interface BalanceRepository { + fun getEthBalance(address: String): Observable> + + fun getAppcBalance(address: String): Observable> + + fun getCreditsBalance(address: String): Observable> + + fun getStoredEthBalance(walletAddress: String): Single> + + fun getStoredAppcBalance(walletAddress: String): Single> + + fun getStoredCreditsBalance(walletAddress: String): Single> +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceScreenModel.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceScreenModel.kt new file mode 100644 index 00000000000..2bf1f76f7f3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/BalanceScreenModel.kt @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.ui.balance + +import com.asfoundation.wallet.ui.iab.FiatValue + +data class BalanceScreenModel( + val overallFiat: FiatValue, + val creditsBalance: TokenBalance, + val appcBalance: TokenBalance, + val ethBalance: TokenBalance +) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/QrCodeActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/QrCodeActivity.kt new file mode 100644 index 00000000000..24f60c2c741 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/QrCodeActivity.kt @@ -0,0 +1,105 @@ +package com.asfoundation.wallet.ui.balance + +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.app.ShareCompat +import androidx.core.content.res.ResourcesCompat +import com.asf.wallet.R +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.ui.BaseActivity +import com.asfoundation.wallet.ui.MyAddressActivity +import com.asfoundation.wallet.util.generateQrCode +import com.google.android.material.snackbar.Snackbar +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.AndroidInjection +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.copy_share_buttons_layout.* +import kotlinx.android.synthetic.main.qr_code_layout.* +import javax.inject.Inject + +class QrCodeActivity : BaseActivity(), QrCodeView { + + @Inject + lateinit var findDefaultWalletInteract: FindDefaultWalletInteract + private lateinit var presenter: QrCodePresenter + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.qr_code_layout) + presenter = + QrCodePresenter(this, findDefaultWalletInteract, CompositeDisposable(), + AndroidSchedulers.mainThread()) + presenter.present() + } + + override fun onResume() { + super.onResume() + sendPageViewEvent() + } + + override fun copyClick() = RxView.clicks(copy_button) + + override fun shareClick() = RxView.clicks(share_button) + + override fun closeClick() = RxView.clicks(close_btn) + + override fun setAddressToClipBoard(walletAddress: String) { + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? + val clip = ClipData.newPlainText(MyAddressActivity.KEY_ADDRESS, walletAddress) + clipboard?.setPrimaryClip(clip) + + Snackbar.make(main_layout, R.string.wallets_address_copied_body, Snackbar.LENGTH_SHORT) + .show() + } + + override fun setWalletAddress(walletAddress: String) { + active_wallet_address.text = walletAddress + } + + override fun createQrCode(walletAddress: String) { + try { + val logo = ResourcesCompat.getDrawable(resources, R.drawable.ic_appc_token, null) + val mergedQrCode = walletAddress.generateQrCode(windowManager, logo!!) + qr_image.setImageBitmap(mergedQrCode) + } catch (e: Exception) { + Snackbar.make(main_layout, getString(R.string.error_fail_generate_qr), Snackbar.LENGTH_SHORT) + .show() + } + } + + override fun showShare(walletAddress: String) { + ShareCompat.IntentBuilder.from(this) + .setText(walletAddress) + .setType("text/plain") + .setChooserTitle(resources.getString(R.string.referral_share_sheet_title)) + .startChooser() + } + + override fun onBackPressed() { + closeSuccess() + super.onBackPressed() + } + + override fun onDestroy() { + presenter.stop() + super.onDestroy() + } + + override fun closeSuccess() { + setResult(Activity.RESULT_OK, Intent()) + finish() + } + + companion object { + @JvmStatic + fun newIntent(context: Context): Intent { + return Intent(context, QrCodeActivity::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/QrCodePresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/QrCodePresenter.kt new file mode 100644 index 00000000000..4f677dcfe86 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/QrCodePresenter.kt @@ -0,0 +1,61 @@ +package com.asfoundation.wallet.ui.balance + +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +class QrCodePresenter( + val view: QrCodeView, + val findDefaultWalletInteract: FindDefaultWalletInteract, + val disposable: CompositeDisposable, + val viewScheduler: Scheduler) { + + fun present() { + requestActiveWalletAddress() + handleCloseClick() + handleCopyClick() + handleShareClick() + } + + private fun requestActiveWalletAddress() { + disposable.add( + findDefaultWalletInteract.find() + .observeOn(viewScheduler) + .doOnSuccess { + view.setWalletAddress(it.address) + view.createQrCode(it.address) + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleCopyClick() { + disposable.add( + view.copyClick() + .flatMapSingle { findDefaultWalletInteract.find() } + .observeOn(viewScheduler) + .doOnNext { view.setAddressToClipBoard(it.address) } + .subscribe()) + } + + private fun handleShareClick() { + disposable.add( + view.shareClick() + .flatMapSingle { findDefaultWalletInteract.find() } + .observeOn(viewScheduler) + .doOnNext { view.showShare(it.address) } + .subscribe()) + + } + + private fun handleCloseClick() { + disposable.add( + view.closeClick() + .observeOn(viewScheduler) + .doOnNext { view.closeSuccess() } + .subscribe()) + } + + fun stop() { + disposable.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/QrCodeView.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/QrCodeView.kt new file mode 100644 index 00000000000..2272bee4234 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/QrCodeView.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.ui.balance + +import io.reactivex.Observable + +interface QrCodeView { + + fun shareClick(): Observable + fun copyClick(): Observable + fun closeSuccess() + fun closeClick(): Observable + fun setAddressToClipBoard(walletAddress: String) + fun showShare(walletAddress: String) + fun setWalletAddress(walletAddress: String) + fun createQrCode(walletAddress: String) +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletActivity.kt new file mode 100644 index 00000000000..638bfd544a2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletActivity.kt @@ -0,0 +1,181 @@ +package com.asfoundation.wallet.ui.balance + +import android.Manifest +import android.animation.Animator +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 android.os.Bundle +import android.provider.DocumentsContract.EXTRA_INITIAL_URI +import android.view.MenuItem +import android.view.View +import android.view.animation.AnimationUtils +import android.view.inputmethod.InputMethodManager +import androidx.core.app.ActivityCompat +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.router.TransactionsRouter +import com.asfoundation.wallet.ui.BaseActivity +import com.google.android.material.snackbar.Snackbar +import dagger.android.AndroidInjection +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.remove_wallet_activity_layout.* +import kotlinx.android.synthetic.main.restore_wallet_layout.* +import javax.inject.Inject + + +class RestoreWalletActivity : BaseActivity(), RestoreWalletActivityView { + + companion object { + private const val RC_READ_EXTERNAL_PERMISSION_CODE = 1002 + private const val FILE_INTENT_CODE = 1003 + + @JvmStatic + fun newIntent(context: Context) = Intent(context, RestoreWalletActivity::class.java) + } + + private lateinit var presenter: RestoreWalletActivityPresenter + private var fileChosenSubject: PublishSubject? = null + private var onPermissionSubject: PublishSubject? = null + + @Inject + lateinit var walletsEventSender: WalletsEventSender + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + fileChosenSubject = PublishSubject.create() + onPermissionSubject = PublishSubject.create() + presenter = RestoreWalletActivityPresenter(walletsEventSender) + setContentView(R.layout.restore_wallet_layout) + toolbar() + if (savedInstanceState == null) navigateToInitialRestoreFragment() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + presenter.sendBackEvent() + if (wallet_remove_animation == null || wallet_remove_animation.visibility != View.VISIBLE) super.onBackPressed() + return true + } + return super.onOptionsItemSelected(item) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == FILE_INTENT_CODE && resultCode == Activity.RESULT_OK && data != null) { + val fileUri = data.data ?: Uri.parse("") + fileChosenSubject?.onNext(fileUri) + } + } + + override fun onBackPressed() { + if (wallet_remove_animation == null || wallet_remove_animation.visibility != View.VISIBLE) super.onBackPressed() + } + + override fun navigateToPasswordView(keystore: String) { + presenter.currentFragment = RestoreWalletPasswordFragment::class.java.simpleName + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, RestoreWalletPasswordFragment.newInstance(keystore)) + .addToBackStack(RestoreWalletPasswordFragment::class.java.simpleName) + .commit() + } + + override fun showWalletRestoreAnimation() { + import_wallet_animation_group.visibility = View.VISIBLE + background.visibility = View.VISIBLE + background.animation = AnimationUtils.loadAnimation(this, R.anim.fast_fade_in_animation) + import_wallet_animation.playAnimation() + } + + override fun showWalletRestoredAnimation() { + import_wallet_animation.setAnimation(R.raw.success_animation) + import_wallet_text.text = getText(R.string.provide_wallet_created_header) + import_wallet_text.visibility = View.VISIBLE + import_wallet_animation.addAnimatorListener(object : Animator.AnimatorListener { + override fun onAnimationRepeat(animation: Animator?) = Unit + override fun onAnimationEnd(animation: Animator?) = navigateToTransactions() + override fun onAnimationCancel(animation: Animator?) = Unit + override fun onAnimationStart(animation: Animator?) = Unit + }) + import_wallet_animation.repeatCount = 0 + import_wallet_animation.playAnimation() + } + + override fun launchFileIntent(path: Uri?) { + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + type = "*/*" + path?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) putExtra(EXTRA_INITIAL_URI, it) + } + } + try { + startActivityForResult(Intent.createChooser(intent, getString(R.string.import_wallet_title)), + FILE_INTENT_CODE) + } catch (ex: ActivityNotFoundException) { + Snackbar.make(main_view, R.string.unknown_error, Snackbar.LENGTH_SHORT) + .show() + } + } + + override fun hideAnimation() { + import_wallet_animation.cancelAnimation() + import_wallet_animation_group.visibility = View.GONE + } + + override fun onFileChosen() = fileChosenSubject!! + + override fun askForReadPermissions() { + if (ActivityCompat.checkSelfPermission(this, + Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + onPermissionSubject?.onNext(Unit) + } else { + requestStorageReadPermission() + } + } + + override fun hideKeyboard() { + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.hideSoftInputFromWindow(main_view.windowToken, 0) + } + + private fun requestStorageReadPermission() { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + RC_READ_EXTERNAL_PERMISSION_CODE) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, + grantResults: IntArray) { + if (requestCode == RC_READ_EXTERNAL_PERMISSION_CODE) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + onPermissionSubject?.onNext(Unit) + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + } + + override fun onPermissionsGiven() = onPermissionSubject!! + + override fun onDestroy() { + onPermissionSubject = null + fileChosenSubject = null + super.onDestroy() + } + + private fun navigateToTransactions() { + TransactionsRouter().open(this, true) + finish() + } + + private fun navigateToInitialRestoreFragment() { + presenter.currentFragment = RestoreWalletFragment::class.java.simpleName + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, RestoreWalletFragment.newInstance()) + .commit() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletActivityPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletActivityPresenter.kt new file mode 100644 index 00000000000..4ad1510f636 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletActivityPresenter.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.ui.balance + +import com.asfoundation.wallet.billing.analytics.WalletsAnalytics +import com.asfoundation.wallet.billing.analytics.WalletsEventSender + +class RestoreWalletActivityPresenter(private val walletsEventSender: WalletsEventSender) { + + var currentFragment: String = RestoreWalletFragment::class.java.simpleName + + private fun sendWalletImportRestoreBackEvent() { + walletsEventSender.sendWalletImportRestoreEvent(WalletsAnalytics.ACTION_BACK, + WalletsAnalytics.STATUS_FAIL, WalletsAnalytics.REASON_CANCELED) + } + + private fun sendWalletPasswordRestoreBackEvent() { + walletsEventSender.sendWalletPasswordRestoreEvent(WalletsAnalytics.ACTION_BACK, + WalletsAnalytics.STATUS_FAIL, WalletsAnalytics.REASON_CANCELED) + } + + fun sendBackEvent() { + when (currentFragment) { + RestoreWalletFragment::class.java.simpleName -> sendWalletImportRestoreBackEvent() + RestoreWalletPasswordFragment::class.java.simpleName -> sendWalletPasswordRestoreBackEvent() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletActivityView.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletActivityView.kt new file mode 100644 index 00000000000..fcd84463312 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletActivityView.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.ui.balance + +import android.net.Uri +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject + +interface RestoreWalletActivityView { + + fun navigateToPasswordView(keystore: String) + + fun showWalletRestoreAnimation() + + fun showWalletRestoredAnimation() + + fun launchFileIntent(path: Uri?) + + fun hideAnimation() + + fun onFileChosen(): Observable + + fun askForReadPermissions() + + fun onPermissionsGiven(): PublishSubject + + fun hideKeyboard() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletFragment.kt new file mode 100644 index 00000000000..129b054ec90 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletFragment.kt @@ -0,0 +1,120 @@ +package com.asfoundation.wallet.ui.balance + +import android.content.Context +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.interact.RestoreWalletInteractor +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.util.RestoreErrorType +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.fragment_restore_wallet_first_layout.* +import javax.inject.Inject + +class RestoreWalletFragment : DaggerFragment(), RestoreWalletView { + + @Inject + lateinit var restoreWalletInteractor: RestoreWalletInteractor + + @Inject + lateinit var walletsEventSender: WalletsEventSender + + @Inject + lateinit var logger: Logger + private lateinit var activityView: RestoreWalletActivityView + private lateinit var presenter: RestoreWalletPresenter + + companion object { + private const val KEYSTORE = "keystore" + + @JvmStatic + fun newInstance() = RestoreWalletFragment() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = + RestoreWalletPresenter(this, activityView, CompositeDisposable(), restoreWalletInteractor, + walletsEventSender, logger, AndroidSchedulers.mainThread(), Schedulers.computation()) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is RestoreWalletActivityView) { + throw IllegalStateException( + "Restore Wallet fragment must be attached to Restore Wallet Activity") + } + activityView = context + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_restore_wallet_first_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setTextChangeListener() + savedInstanceState?.let { keystore_edit_text.setText(it.getString(KEYSTORE, "")) } + presenter.present() + } + + override fun restoreFromStringClick(): Observable = RxView.clicks(import_wallet_button) + .map { keystore_edit_text.editableText.toString() } + + override fun restoreFromFileClick() = RxView.clicks(import_from_file_button) + + + override fun showError(type: RestoreErrorType) { + label_input.isErrorEnabled = true + when (type) { + RestoreErrorType.ALREADY_ADDED -> label_input.error = getString(R.string.error_already_added) + RestoreErrorType.INVALID_KEYSTORE -> label_input.error = getString(R.string.error_import) + else -> label_input.error = getString(R.string.error_general) + } + } + + override fun navigateToPasswordView(keystore: String) { + activityView.navigateToPasswordView(keystore) + } + + private fun setTextChangeListener() { + keystore_edit_text.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, + i2: Int) { + } + + override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, + i2: Int) { + } + + override fun afterTextChanged(editable: Editable) { + label_input.isErrorEnabled = false + import_wallet_button.isEnabled = editable.toString() + .isNotEmpty() + } + }) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + if (keystore_edit_text != null) { + outState.putString(KEYSTORE, keystore_edit_text.editableText.toString()) + } + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPasswordFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPasswordFragment.kt new file mode 100644 index 00000000000..329112118d5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPasswordFragment.kt @@ -0,0 +1,135 @@ +package com.asfoundation.wallet.ui.balance + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.RestoreErrorType +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.restore_wallet_password_layout.* +import kotlinx.android.synthetic.main.wallet_outlined_card.* +import javax.inject.Inject + +class RestoreWalletPasswordFragment : DaggerFragment(), RestoreWalletPasswordView { + + @Inject + lateinit var restoreWalletPasswordInteractor: RestoreWalletPasswordInteractor + + @Inject + lateinit var walletsEventSender: WalletsEventSender + + @Inject + lateinit var currencyFormatUtils: CurrencyFormatUtils + private lateinit var activityView: RestoreWalletActivityView + private lateinit var presenter: RestoreWalletPasswordPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = + RestoreWalletPasswordPresenter(this, activityView, restoreWalletPasswordInteractor, + walletsEventSender, CompositeDisposable(), AndroidSchedulers.mainThread(), + Schedulers.io(), Schedulers.computation()) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is RestoreWalletActivityView) { + throw IllegalStateException( + "Restore Wallet Password fragment must be attached to Restore Wallet Activity") + } + activityView = context + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setTextChangeListener() + presenter.present(keystore) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.restore_wallet_password_layout, container, false) + } + + @SuppressLint("SetTextI18n") + override fun updateUi(address: String, fiatValue: FiatValue) { + wallet_address.text = address + wallet_balance.text = + "${fiatValue.symbol}${currencyFormatUtils.formatCurrency(fiatValue.amount)}" + } + + override fun restoreWalletButtonClick(): Observable { + return RxView.clicks(import_wallet_button) + .map { password_edit_text.editableText.toString() } + } + + override fun showWalletRestoreAnimation() = activityView.showWalletRestoreAnimation() + + override fun showWalletRestoredAnimation() = activityView.showWalletRestoredAnimation() + + override fun hideAnimation() = activityView.hideAnimation() + + override fun showError(type: RestoreErrorType) { + label_input.isErrorEnabled = true + when (type) { + RestoreErrorType.ALREADY_ADDED -> label_input.error = getString(R.string.error_already_added) + RestoreErrorType.INVALID_PASS -> label_input.error = + getString(R.string.import_wallet_wrong_password_body) + RestoreErrorType.INVALID_KEYSTORE -> label_input.error = getString(R.string.error_import) + else -> label_input.error = getString(R.string.error_general) + } + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + private fun setTextChangeListener() { + password_edit_text.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) = Unit + override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) = Unit + override fun afterTextChanged(editable: Editable) { + label_input.isErrorEnabled = false + import_wallet_button.isEnabled = editable.toString() + .isNotEmpty() + } + }) + } + + private val keystore: String by lazy { + if (arguments!!.containsKey(KEYSTORE_KEY)) { + arguments!!.getString(KEYSTORE_KEY)!! + } else { + throw IllegalArgumentException("keystore not found") + } + } + + companion object { + + private const val KEYSTORE_KEY = "keystore" + + fun newInstance(keystore: String): RestoreWalletPasswordFragment { + val fragment = RestoreWalletPasswordFragment() + Bundle().apply { + putString(KEYSTORE_KEY, keystore) + fragment.arguments = this + } + return fragment + } + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPasswordInteractor.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPasswordInteractor.kt new file mode 100644 index 00000000000..df2d334090c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPasswordInteractor.kt @@ -0,0 +1,29 @@ +package com.asfoundation.wallet.ui.balance + +import com.asfoundation.wallet.interact.RestoreWalletInteractor +import com.asfoundation.wallet.interact.WalletModel +import com.asfoundation.wallet.ui.iab.FiatValue +import com.google.gson.Gson +import io.reactivex.Observable +import io.reactivex.Single + +class RestoreWalletPasswordInteractor(private val gson: Gson, + private val balanceInteract: BalanceInteract, + private val restoreWalletInteractor: RestoreWalletInteractor) { + + fun extractWalletAddress(keystore: String): Single = Single.create { + val parsedKeystore = gson.fromJson(keystore, Keystore::class.java) + it.onSuccess("0x" + parsedKeystore.address) + } + + fun getOverallBalance(address: String): Observable = + balanceInteract.getTotalBalance(address) + + fun restoreWallet(keystore: String, password: String): Single = + restoreWalletInteractor.restoreKeystore(keystore, password) + + fun setDefaultWallet(address: String) = restoreWalletInteractor.setDefaultWallet(address) + + private data class Keystore(val address: String) + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPasswordPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPasswordPresenter.kt new file mode 100644 index 00000000000..3712b6ef84c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPasswordPresenter.kt @@ -0,0 +1,87 @@ +package com.asfoundation.wallet.ui.balance + +import com.asfoundation.wallet.billing.analytics.WalletsAnalytics +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.interact.WalletModel +import com.asfoundation.wallet.util.RestoreErrorType +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +class RestoreWalletPasswordPresenter(private val view: RestoreWalletPasswordView, + private val activityView: RestoreWalletActivityView, + private val interactor: RestoreWalletPasswordInteractor, + private val walletsEventSender: WalletsEventSender, + private val disposable: CompositeDisposable, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val computationScheduler: Scheduler) { + + fun present(keystore: String) { + populateUi(keystore) + handleRestoreWalletButtonClicked(keystore) + } + + private fun populateUi(keystore: String) { + disposable.add(interactor.extractWalletAddress(keystore) + .subscribeOn(networkScheduler) + .flatMapObservable { address -> + interactor.getOverallBalance(address) + .observeOn(viewScheduler) + .doOnNext { fiatValue -> view.updateUi(address, fiatValue) } + } + .observeOn(viewScheduler) + .subscribe({}, { + it.printStackTrace() + view.showError(RestoreErrorType.GENERIC) + })) + } + + private fun handleRestoreWalletButtonClicked(keystore: String) { + disposable.add(view.restoreWalletButtonClick() + .doOnNext { + activityView.hideKeyboard() + view.showWalletRestoreAnimation() + } + .doOnNext { + walletsEventSender.sendWalletPasswordRestoreEvent(WalletsAnalytics.ACTION_IMPORT, + WalletsAnalytics.STATUS_SUCCESS) + } + .doOnError { + walletsEventSender.sendWalletPasswordRestoreEvent(WalletsAnalytics.ACTION_IMPORT, + WalletsAnalytics.STATUS_FAIL, it.message) + } + .observeOn(computationScheduler) + .flatMapSingle { interactor.restoreWallet(keystore, it) } + .observeOn(viewScheduler) + .doOnNext { handleWalletModel(it) } + .doOnError { + walletsEventSender.sendWalletCompleteRestoreEvent(WalletsAnalytics.STATUS_FAIL, + it.message) + } + .subscribe({}, { + it.printStackTrace() + view.hideAnimation() + view.showError(RestoreErrorType.GENERIC) + })) + } + + private fun setDefaultWallet(address: String) { + disposable.add(interactor.setDefaultWallet(address) + .doOnComplete { view.showWalletRestoredAnimation() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleWalletModel(walletModel: WalletModel) { + if (walletModel.error.hasError) { + view.hideAnimation() + view.showError(walletModel.error.type) + walletsEventSender.sendWalletCompleteRestoreEvent(WalletsAnalytics.STATUS_FAIL, + walletModel.error.type.toString()) + } else { + setDefaultWallet(walletModel.address) + walletsEventSender.sendWalletCompleteRestoreEvent(WalletsAnalytics.STATUS_SUCCESS) + } + } + + fun stop() = disposable.clear() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPasswordView.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPasswordView.kt new file mode 100644 index 00000000000..886bc7dfb12 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPasswordView.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.ui.balance + +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.util.RestoreErrorType +import io.reactivex.Observable + +interface RestoreWalletPasswordView { + + fun updateUi(address: String, fiatValue: FiatValue) + fun restoreWalletButtonClick(): Observable + fun showWalletRestoreAnimation() + fun showWalletRestoredAnimation() + fun showError(type: RestoreErrorType) + fun hideAnimation() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPresenter.kt new file mode 100644 index 00000000000..10df6ed29b4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletPresenter.kt @@ -0,0 +1,120 @@ +package com.asfoundation.wallet.ui.balance + +import com.asfoundation.wallet.billing.analytics.WalletsAnalytics +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.interact.RestoreWalletInteractor +import com.asfoundation.wallet.interact.WalletModel +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.util.RestoreError +import com.asfoundation.wallet.util.RestoreErrorType +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable + +class RestoreWalletPresenter(private val view: RestoreWalletView, + private val activityView: RestoreWalletActivityView, + private val disposable: CompositeDisposable, + private val restoreWalletInteractor: RestoreWalletInteractor, + private val walletsEventSender: WalletsEventSender, + private val logger: Logger, + private val viewScheduler: Scheduler, + private val computationScheduler: Scheduler) { + + fun present() { + handleRestoreFromString() + handleRestoreFromFile() + handleFileChosen() + handleOnPermissionsGiven() + } + + private fun handleOnPermissionsGiven() { + disposable.add(activityView.onPermissionsGiven() + .doOnNext { activityView.launchFileIntent(restoreWalletInteractor.getPath()) } + .subscribe()) + } + + private fun handleFileChosen() { + disposable.add(activityView.onFileChosen() + .doOnNext { activityView.showWalletRestoreAnimation() } + .flatMapSingle { restoreWalletInteractor.readFile(it) } + .observeOn(computationScheduler) + .flatMapSingle { fetchWalletModel(it) } + .observeOn(viewScheduler) + .doOnNext { handleWalletModel(it) } + .subscribe({}, { + logger.log("RestoreWalletPresenter", it) + activityView.hideAnimation() + view.showError(RestoreErrorType.INVALID_KEYSTORE) + }) + ) + } + + private fun handleRestoreFromFile() { + disposable.add(view.restoreFromFileClick() + .doOnNext { activityView.askForReadPermissions() } + .doOnNext { + walletsEventSender.sendWalletImportRestoreEvent(WalletsAnalytics.ACTION_IMPORT_FROM_FILE, + WalletsAnalytics.STATUS_SUCCESS) + } + .doOnError { t -> + walletsEventSender.sendWalletImportRestoreEvent(WalletsAnalytics.ACTION_IMPORT_FROM_FILE, + WalletsAnalytics.STATUS_FAIL, t.message) + } + .subscribe()) + } + + private fun handleRestoreFromString() { + disposable.add(view.restoreFromStringClick() + .doOnNext { + activityView.hideKeyboard() + activityView.showWalletRestoreAnimation() + } + .observeOn(computationScheduler) + .flatMapSingle { fetchWalletModel(it) } + .observeOn(viewScheduler) + .doOnNext { handleWalletModel(it) } + .doOnError { t -> + walletsEventSender.sendWalletImportRestoreEvent(WalletsAnalytics.ACTION_IMPORT, + WalletsAnalytics.STATUS_FAIL, t.message) + } + .subscribe()) + } + + private fun setDefaultWallet(address: String) { + disposable.add(restoreWalletInteractor.setDefaultWallet(address) + .doOnComplete { activityView.showWalletRestoredAnimation() } + .subscribe()) + } + + private fun handleWalletModel(walletModel: WalletModel) { + if (walletModel.error.hasError) { + activityView.hideAnimation() + if (walletModel.error.type == RestoreErrorType.INVALID_PASS) { + view.navigateToPasswordView(walletModel.keystore) + walletsEventSender.sendWalletImportRestoreEvent(WalletsAnalytics.ACTION_IMPORT, + WalletsAnalytics.STATUS_SUCCESS) + } else { + view.showError(walletModel.error.type) + walletsEventSender.sendWalletImportRestoreEvent(WalletsAnalytics.ACTION_IMPORT, + WalletsAnalytics.STATUS_FAIL, walletModel.error.type.toString()) + } + } else { + setDefaultWallet(walletModel.address) + walletsEventSender.sendWalletImportRestoreEvent(WalletsAnalytics.ACTION_IMPORT, + WalletsAnalytics.STATUS_SUCCESS) + } + } + + private fun fetchWalletModel(key: String): Single { + return if (restoreWalletInteractor.isKeystore(key)) restoreWalletInteractor.restoreKeystore(key) + else { + if (key.length == 64) restoreWalletInteractor.restorePrivateKey(key) + else Single.just(WalletModel(RestoreError(RestoreErrorType.INVALID_PRIVATE_KEY))) + } + } + + fun stop() { + disposable.clear() + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletView.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletView.kt new file mode 100644 index 00000000000..f6d0032e63e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/RestoreWalletView.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.ui.balance + +import com.asfoundation.wallet.util.RestoreErrorType +import io.reactivex.Observable + +interface RestoreWalletView { + + fun restoreFromStringClick(): Observable + + fun restoreFromFileClick(): Observable + + fun navigateToPasswordView(keystore: String) + + fun showError(type: RestoreErrorType) +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/TokenBalance.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/TokenBalance.kt new file mode 100644 index 00000000000..b8cc439805c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/TokenBalance.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.ui.balance + +import com.asfoundation.wallet.ui.TokenValue +import com.asfoundation.wallet.ui.iab.FiatValue + +data class TokenBalance(val token: TokenValue, val fiat: FiatValue) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/TokenDetailsActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/TokenDetailsActivity.kt new file mode 100644 index 00000000000..766cfb93b5f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/TokenDetailsActivity.kt @@ -0,0 +1,159 @@ +package com.asfoundation.wallet.ui.balance + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.transition.Transition +import android.view.View +import android.view.Window +import com.asf.wallet.R +import com.asfoundation.wallet.router.TopUpRouter +import com.asfoundation.wallet.ui.BaseActivity +import com.asfoundation.wallet.util.WalletCurrency +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.activity_token_details.* + + +class TokenDetailsActivity : BaseActivity(), TokenDetailsView { + + private var contentVisible = false + private lateinit var token: TokenDetailsId + private lateinit var presenter: TokenDetailsPresenter + + override fun onResume() { + super.onResume() + sendPageViewEvent() + } + + enum class TokenDetailsId { + ETHER, APPC, APPC_CREDITS + } + + companion object { + @JvmStatic + fun newInstance(context: Context, tokenDetailsId: TokenDetailsId): Intent { + val intent = Intent(context, TokenDetailsActivity::class.java) + intent.putExtra(KEY_CONTENT, tokenDetailsId) + return intent + } + + private const val KEY_CONTENT = "KEY_CONTENT" + private const val PARAM_ENTERING = "PARAM_ENTERING" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) + setContentView(R.layout.activity_token_details) + presenter = TokenDetailsPresenter(this, CompositeDisposable()) + savedInstanceState?.let { + contentVisible = it.getBoolean(PARAM_ENTERING) + } + presenter.present() + } + + override fun onDestroy() { + presenter.stop() + super.onDestroy() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(PARAM_ENTERING, contentVisible) + } + + private fun setContent(tokenDetailsId: TokenDetailsId) { + when (tokenDetailsId) { + TokenDetailsId.ETHER -> { + token_icon.setImageResource(R.drawable.ic_eth_token) + token_name.text = getString(R.string.ethereum_token_name) + token_symbol.text = "(${WalletCurrency.ETHEREUM.symbol})" + token_description.text = getString(R.string.balance_ethereum_body) + } + TokenDetailsId.APPC -> { + token_icon.setImageResource(R.drawable.ic_appc_token) + token_name.text = getString(R.string.appc_token_name) + token_symbol.text = "(${WalletCurrency.APPCOINS.symbol})" + token_description.text = getString(R.string.balance_appcoins_body) + } + TokenDetailsId.APPC_CREDITS -> { + token_icon.setImageResource(R.drawable.ic_appc_c_token) + token_name.text = getString(R.string.appc_credits_token_name) + token_symbol.text = "(${WalletCurrency.CREDITS.symbol})" + token_description.text = getString(R.string.balance_appccreditos_body) + } + } + } + + override fun onBackPressed() { + token_symbol.visibility = View.INVISIBLE + token_description.visibility = View.INVISIBLE + close_btn.visibility = View.INVISIBLE + topup_btn.visibility = View.INVISIBLE + super.onBackPressed() + } + + override fun setupUi() { + intent.extras?.let { + if (it.containsKey( + KEY_CONTENT)) { + token = it.getSerializable(KEY_CONTENT) as TokenDetailsId + setContent(token) + } + } + + val sharedElementEnterTransition = window.sharedElementEnterTransition + sharedElementEnterTransition.addListener(object : Transition.TransitionListener { + override fun onTransitionStart(transition: Transition) { + } + + override fun onTransitionEnd(transition: Transition) { + if (!contentVisible) { + token_symbol.visibility = View.VISIBLE + token_description.visibility = View.VISIBLE + close_btn.visibility = View.VISIBLE + if (token == TokenDetailsId.APPC_CREDITS) { + topup_btn.visibility = View.VISIBLE + } + contentVisible = true + } + } + + override fun onTransitionCancel(transition: Transition) { + } + + override fun onTransitionPause(transition: Transition) { + } + + override fun onTransitionResume(transition: Transition) { + } + }) + + if (contentVisible) { + token_symbol.visibility = View.VISIBLE + token_description.visibility = View.VISIBLE + close_btn.visibility = View.VISIBLE + if (token == TokenDetailsId.APPC_CREDITS) { + topup_btn.visibility = View.VISIBLE + } + } + } + + override fun getOkClick(): Observable { + return RxView.clicks(close_btn) + } + + override fun close() { + onBackPressed() + } + + override fun getTopUpClick(): Observable { + return RxView.clicks(topup_btn) + } + + override fun showTopUp() { + TopUpRouter().open(this) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/TokenDetailsPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/TokenDetailsPresenter.kt new file mode 100644 index 00000000000..cd499ba9d9b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/TokenDetailsPresenter.kt @@ -0,0 +1,34 @@ +package com.asfoundation.wallet.ui.balance + +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.TimeUnit + +class TokenDetailsPresenter(private val view: TokenDetailsView, + private val disposables: CompositeDisposable) { + fun present() { + view.setupUi() + handleOkClick() + handleTopUpClick() + } + + fun stop() { + disposables.dispose() + } + + private fun handleOkClick() { + disposables.add( + view.getOkClick() + .doOnNext { view.close() } + .subscribe() + ) + } + + private fun handleTopUpClick() { + disposables.add( + view.getTopUpClick() + .throttleFirst(1, TimeUnit.SECONDS) + .doOnNext { view.showTopUp() } + .subscribe() + ) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/TokenDetailsView.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/TokenDetailsView.kt new file mode 100644 index 00000000000..e360026c882 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/TokenDetailsView.kt @@ -0,0 +1,17 @@ +package com.asfoundation.wallet.ui.balance + +import io.reactivex.Observable + +interface TokenDetailsView { + + fun setupUi() + + fun getOkClick(): Observable + + fun close() + + fun getTopUpClick(): Observable + + fun showTopUp() + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/TransactionDetailActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/balance/TransactionDetailActivity.java new file mode 100644 index 00000000000..9d2bf278689 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/TransactionDetailActivity.java @@ -0,0 +1,330 @@ +package com.asfoundation.wallet.ui.balance; + +import android.app.Dialog; +import android.os.Bundle; +import android.text.format.DateFormat; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.RecyclerView; +import com.asf.wallet.R; +import com.asfoundation.wallet.GlideApp; +import com.asfoundation.wallet.entity.NetworkInfo; +import com.asfoundation.wallet.entity.Wallet; +import com.asfoundation.wallet.transactions.Operation; +import com.asfoundation.wallet.transactions.Transaction; +import com.asfoundation.wallet.transactions.TransactionDetails; +import com.asfoundation.wallet.ui.BaseActivity; +import com.asfoundation.wallet.ui.toolbar.ToolbarArcBackground; +import com.asfoundation.wallet.ui.widget.adapter.TransactionsDetailsAdapter; +import com.asfoundation.wallet.util.BalanceUtils; +import com.asfoundation.wallet.util.CurrencyFormatUtils; +import com.asfoundation.wallet.util.WalletCurrency; +import com.asfoundation.wallet.viewmodel.TransactionDetailViewModel; +import com.asfoundation.wallet.viewmodel.TransactionDetailViewModelFactory; +import com.bumptech.glide.load.resource.bitmap.CircleCrop; +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.appbar.AppBarLayout; +import dagger.android.AndroidInjection; +import io.reactivex.disposables.CompositeDisposable; +import java.math.BigDecimal; +import java.util.Calendar; +import java.util.Locale; +import javax.inject.Inject; + +import static com.asfoundation.wallet.C.Key.TRANSACTION; + +public class TransactionDetailActivity extends BaseActivity { + + private static final int DECIMALS = 18; + @Inject TransactionDetailViewModelFactory transactionDetailViewModelFactory; + @Inject CurrencyFormatUtils formatter; + private TransactionDetailViewModel viewModel; + private Transaction transaction; + private boolean isSent = false; + private TextView amount; + private TransactionsDetailsAdapter adapter; + private RecyclerView detailsList; + private Dialog dialog; + private CompositeDisposable disposables; + + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + AndroidInjection.inject(this); + + setContentView(R.layout.activity_transaction_detail); + findViewById(R.id.more_detail).setVisibility(View.GONE); + + disposables = new CompositeDisposable(); + transaction = getIntent().getParcelableExtra(TRANSACTION); + if (transaction == null) { + finish(); + return; + } + toolbar(); + + ((ToolbarArcBackground) findViewById(R.id.toolbar_background_arc)).setScale(1f); + + amount = findViewById(R.id.amount); + adapter = new TransactionsDetailsAdapter(this::onMoreClicked); + detailsList = findViewById(R.id.details_list); + detailsList.setAdapter(adapter); + + viewModel = ViewModelProviders.of(this, transactionDetailViewModelFactory) + .get(TransactionDetailViewModel.class); + viewModel.defaultNetwork() + .observe(this, this::onDefaultNetwork); + viewModel.defaultWallet() + .observe(this, this::onDefaultWallet); + + ((AppBarLayout) findViewById(R.id.app_bar)).addOnOffsetChangedListener( + (appBarLayout, verticalOffset) -> { + float percentage = + 1 - ((float) Math.abs(verticalOffset) / appBarLayout.getTotalScrollRange()); + findViewById(R.id.img).setScaleX(percentage); + findViewById(R.id.img).setScaleY(percentage); + }); + } + + @Override protected void onResume() { + super.onResume(); + sendPageViewEvent(); + } + + @Override protected void onStop() { + super.onStop(); + disposables.dispose(); + hideDialog(); + } + + private void onDefaultWallet(Wallet wallet) { + adapter.setDefaultWallet(wallet); + + if (transaction.getOperations() != null && !transaction.getOperations() + .isEmpty()) { + adapter.addOperations(transaction.getOperations()); + detailsList.setVisibility(View.VISIBLE); + } + + isSent = transaction.getFrom() + .toLowerCase() + .equals(wallet.address); + + NetworkInfo networkInfo = viewModel.defaultNetwork() + .getValue(); + + String symbol = + transaction.getCurrency() == null ? (networkInfo == null ? "" : networkInfo.symbol) + : transaction.getCurrency(); + + String icon = null; + String id = null; + String description = null; + String to = null; + TransactionDetails details = transaction.getDetails(); + + if (details != null) { + icon = details.getIcon() + .getUri(); + id = details.getSourceName(); + description = details.getDescription(); + } + + @StringRes int typeStr = R.string.transaction_type_standard; + @DrawableRes int typeIcon = R.drawable.ic_transaction_peer; + View button = findViewById(R.id.more_detail); + View categorybackground = findViewById(R.id.category_icon_background); + + switch (transaction.getType()) { + case ADS: + typeStr = R.string.transaction_type_poa; + typeIcon = R.drawable.ic_transaction_poa; + break; + case ADS_OFFCHAIN: + typeStr = R.string.transaction_type_poa_offchain; + typeIcon = R.drawable.ic_transaction_poa; + button.setVisibility(View.VISIBLE); + button.setOnClickListener( + view -> viewModel.showMoreDetailsBds(view.getContext(), transaction)); + symbol = getString(R.string.p2p_send_currency_appc_c); + break; + case IAP_OFFCHAIN: + button.setVisibility(View.VISIBLE); + to = transaction.getTo(); + typeStr = R.string.transaction_type_iab; + typeIcon = R.drawable.ic_transaction_iab; + button.setOnClickListener( + view -> viewModel.showMoreDetailsBds(view.getContext(), transaction)); + break; + case BONUS: + button.setVisibility(View.VISIBLE); + typeStr = R.string.transaction_type_bonus; + typeIcon = -1; + if (transaction.getDetails() + .getSourceName() == null) { + id = getString(R.string.transaction_type_bonus); + } else { + id = getString(R.string.gamification_level_bonus, transaction.getDetails() + .getSourceName()); + } + button.setOnClickListener( + view -> viewModel.showMoreDetailsBds(view.getContext(), transaction)); + symbol = getString(R.string.p2p_send_currency_appc_c); + break; + case TOP_UP: + typeStr = R.string.topup_title; + id = getString(R.string.topup_title); + categorybackground.setBackground(null); + typeIcon = R.drawable.transaction_type_top_up; + button.setVisibility(View.VISIBLE); + button.setOnClickListener( + view -> viewModel.showMoreDetailsBds(view.getContext(), transaction)); + symbol = getString(R.string.p2p_send_currency_appc_c); + break; + case TRANSFER_OFF_CHAIN: + typeStr = R.string.transaction_type_p2p; + id = isSent ? "Transfer Sent" : getString(R.string.askafriend_received_title); + typeIcon = R.drawable.transaction_type_transfer_off_chain; + categorybackground.setBackground(null); + to = transaction.getTo(); + button.setVisibility(View.VISIBLE); + button.setOnClickListener( + view -> viewModel.showMoreDetailsBds(view.getContext(), transaction)); + symbol = getString(R.string.p2p_send_currency_appc_c); + break; + } + + @StringRes int statusStr = R.string.transaction_status_success; + @ColorRes int statusColor = R.color.green; + + switch (transaction.getStatus()) { + case FAILED: + statusStr = R.string.transaction_status_failed; + statusColor = R.color.red; + break; + case PENDING: + statusStr = R.string.transaction_status_pending; + statusColor = R.color.orange; + break; + } + + setUIContent(transaction.getTimeStamp(), getValue(symbol), symbol, icon, id, description, + typeStr, typeIcon, statusStr, statusColor, to, isSent); + } + + private void onDefaultNetwork(NetworkInfo networkInfo) { + adapter.setDefaultNetwork(networkInfo); + + String symbol = + transaction.getCurrency() == null ? (networkInfo == null ? "" : networkInfo.symbol) + : transaction.getCurrency(); + formatValue(getValue(symbol), symbol); + } + + private String getScaledValue(String valueStr, String currencySymbol) { + WalletCurrency walletCurrency = WalletCurrency.mapToWalletCurrency(currencySymbol); + BigDecimal value = new BigDecimal(valueStr); + value = value.divide(new BigDecimal(Math.pow(10, DECIMALS))); + return formatter.formatCurrency(value, walletCurrency); + } + + private String getDate(long timeStampInSec) { + Calendar cal = Calendar.getInstance(Locale.ENGLISH); + cal.setTimeInMillis(timeStampInSec); + return DateFormat.format("dd MMM yyyy hh:mm a", cal.getTime()) + .toString(); + } + + private void onMoreClicked(View view, Operation operation) { + viewModel.showMoreDetails(view.getContext(), operation); + } + + private void setUIContent(long timeStamp, String value, String symbol, String icon, String id, + String description, int typeStr, int typeIcon, int statusStr, int statusColor, String to, + boolean isSent) { + ((TextView) findViewById(R.id.transaction_timestamp)).setText(getDate(timeStamp)); + findViewById(R.id.transaction_timestamp).setVisibility(View.VISIBLE); + + formatValue(value, symbol); + + ImageView typeIconImageView = findViewById(R.id.img); + if (icon != null) { + String path; + if (icon.startsWith("http://") || icon.startsWith("https://")) { + path = icon; + } else { + path = "file:" + icon; + } + + GlideApp.with(this) + .load(path) + .apply(RequestOptions.bitmapTransform(new CircleCrop())) + .transition(DrawableTransitionOptions.withCrossFade()) + .into(typeIconImageView); + } else { + if (typeIcon != -1) { + typeIconImageView.setImageResource(typeIcon); + typeIconImageView.setVisibility(View.VISIBLE); + } else { + typeIconImageView.setVisibility(View.GONE); + } + } + + ((TextView) findViewById(R.id.app_id)).setText(id); + if (description != null) { + ((TextView) findViewById(R.id.item_id)).setText(description); + findViewById(R.id.item_id).setVisibility(View.VISIBLE); + } + ((TextView) findViewById(R.id.category_name)).setText(typeStr); + + if (typeIcon != -1) { + ((ImageView) findViewById(R.id.category_icon)).setImageResource(typeIcon); + findViewById(R.id.category_icon).setVisibility(View.VISIBLE); + findViewById(R.id.category_icon_background).setVisibility(View.VISIBLE); + } else { + findViewById(R.id.category_icon_background).setVisibility(View.GONE); + } + + ((TextView) findViewById(R.id.status)).setText(statusStr); + ((TextView) findViewById(R.id.status)).setTextColor(getResources().getColor(statusColor)); + + if (to != null) { + ((TextView) findViewById(R.id.to)).setText( + isSent ? getString(R.string.label_to) : getString(R.string.label_from)); + findViewById(R.id.to_label).setVisibility(View.VISIBLE); + ((TextView) findViewById(R.id.to)).setText(to); + findViewById(R.id.to).setVisibility(View.VISIBLE); + detailsList.setVisibility(View.GONE); + findViewById(R.id.details_label).setVisibility(View.GONE); + } + } + + private void hideDialog() { + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + dialog = null; + } + } + + private void formatValue(String value, String symbol) { + int smallTitleSize = (int) getResources().getDimension(R.dimen.small_text); + int color = getResources().getColor(R.color.color_grey_9e); + String formattedValue = (isSent ? "-" : "+") + value; + amount.setText(BalanceUtils.formatBalance(formattedValue, symbol, smallTitleSize, color)); + } + + private String getValue(String currencySymbol) { + String rawValue = transaction.getValue(); + if (!rawValue.equals("0")) { + rawValue = getScaledValue(rawValue, currencySymbol); + } + return rawValue; + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/database/BalanceDetailsDao.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/database/BalanceDetailsDao.kt new file mode 100644 index 00000000000..f2415e34501 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/database/BalanceDetailsDao.kt @@ -0,0 +1,40 @@ +package com.asfoundation.wallet.ui.balance.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.asfoundation.wallet.ui.balance.database.BalanceDetailsEntity.Companion.TABLE_NAME +import io.reactivex.Observable + + +@Dao +interface BalanceDetailsDao { + + @Query( + "select * from $TABLE_NAME where `wallet_address` like :wallet_address limit 1") + fun getSyncBalance(wallet_address: String): BalanceDetailsEntity? + + @Query( + "select * from $TABLE_NAME where `wallet_address` like :wallet_address limit 1") + fun getBalance(wallet_address: String): Observable + + @Insert(onConflict = OnConflictStrategy.ABORT) + fun insert(data: BalanceDetailsEntity): Long + + @Query( + "UPDATE $TABLE_NAME SET eth_token_amount = :amount, eth_token_conversion = :conversion, fiat_currency = :currency, fiat_symbol = :symbol WHERE wallet_address =:address") + fun updateEthBalance(address: String, amount: String, conversion: String, currency: String, + symbol: String): Int + + @Query( + "UPDATE $TABLE_NAME SET appc_token_amount = :amount, appc_token_conversion = :conversion, fiat_currency = :currency, fiat_symbol = :symbol WHERE wallet_address =:address") + fun updateAppcBalance(address: String, amount: String, conversion: String, currency: String, + symbol: String): Int + + @Query( + "UPDATE $TABLE_NAME SET credits_token_amount = :amount, credits_token_conversion = :conversion, fiat_currency = :currency, fiat_symbol = :symbol WHERE wallet_address =:address") + fun updateCreditsBalance(address: String, amount: String, conversion: String, currency: String, + symbol: String): Int + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/database/BalanceDetailsDatabase.java b/app/src/main/java/com/asfoundation/wallet/ui/balance/database/BalanceDetailsDatabase.java new file mode 100644 index 00000000000..7bcd13e4f56 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/database/BalanceDetailsDatabase.java @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.ui.balance.database; + +import androidx.room.Database; +import androidx.room.RoomDatabase; + +@Database(entities = BalanceDetailsEntity.class, version = 1) +public abstract class BalanceDetailsDatabase extends RoomDatabase { + public abstract BalanceDetailsDao balanceDetailsDao(); +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/database/BalanceDetailsEntity.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/database/BalanceDetailsEntity.kt new file mode 100644 index 00000000000..5b9716a9c99 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/database/BalanceDetailsEntity.kt @@ -0,0 +1,64 @@ +package com.asfoundation.wallet.ui.balance.database + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.asfoundation.wallet.ui.balance.database.BalanceDetailsEntity.Companion.TABLE_NAME + +@Entity(tableName = TABLE_NAME) +class BalanceDetailsEntity(@field:PrimaryKey @field:ColumnInfo(name = "wallet_address") + val wallet: String) { + @ColumnInfo(name = "fiat_currency") + var fiatCurrency: String = "" + @ColumnInfo(name = "fiat_symbol") + var fiatSymbol: String = "" + @ColumnInfo(name = "eth_token_amount") + var ethAmount: String = "" + @ColumnInfo(name = "eth_token_conversion") + var ethConversion: String = "" + @ColumnInfo(name = "appc_token_amount") + var appcAmount: String = "" + @ColumnInfo(name = "appc_token_conversion") + var appcConversion: String = "" + @ColumnInfo(name = "credits_token_amount") + var creditsAmount: String = "" + @ColumnInfo(name = "credits_token_conversion") + var creditsConversion: String = "" + + override fun toString(): String { + return ("BalanceDetailsEntity{" + + "wallet='" + + wallet + + '\''.toString() + + ", fiatCurrency='" + + fiatCurrency + + '\''.toString() + + ", fiatSymbol='" + + fiatSymbol + + '\''.toString() + + ", ethAmount='" + + ethAmount + + '\''.toString() + + ", ethConversion='" + + ethConversion + + '\''.toString() + + ", appcAmount='" + + appcAmount + + '\''.toString() + + ", appcConversion='" + + appcConversion + + '\''.toString() + + ", creditsAmount='" + + creditsAmount + + '\''.toString() + + ", creditsConversion='" + + creditsConversion + + '\''.toString() + + '}'.toString()) + } + + companion object { + + internal const val TABLE_NAME = "balance" + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/balance/database/BalanceDetailsMapper.kt b/app/src/main/java/com/asfoundation/wallet/ui/balance/database/BalanceDetailsMapper.kt new file mode 100644 index 00000000000..b0756e28563 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/balance/database/BalanceDetailsMapper.kt @@ -0,0 +1,42 @@ +package com.asfoundation.wallet.ui.balance.database + +import android.util.Pair +import com.asfoundation.wallet.entity.Balance +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import java.math.BigDecimal +import java.math.RoundingMode +import javax.inject.Inject + + +class BalanceDetailsMapper { + + @Inject + lateinit var formatter: CurrencyFormatUtils + + fun map(walletAddress: String): BalanceDetailsEntity = BalanceDetailsEntity(walletAddress) + + fun mapEthBalance(balance: BalanceDetailsEntity): Pair { + return Pair(Balance(WalletCurrency.ETHEREUM.symbol, getBigDecimal(balance.ethAmount)), + FiatValue(getBigDecimal(balance.ethConversion), balance.fiatCurrency, balance.fiatSymbol)) + } + + fun mapAppcBalance(balance: BalanceDetailsEntity): Pair { + return Pair(Balance(WalletCurrency.APPCOINS.symbol, getBigDecimal(balance.appcAmount, + CurrencyFormatUtils.APPC_SCALE)), FiatValue(getBigDecimal(balance.appcConversion), + balance.fiatCurrency, balance.fiatSymbol)) + } + + fun mapCreditsBalance(balance: BalanceDetailsEntity): Pair { + return Pair(Balance(WalletCurrency.CREDITS.symbol, + getBigDecimal(balance.creditsAmount, CurrencyFormatUtils.CREDITS_SCALE)), + FiatValue(getBigDecimal(balance.creditsConversion), balance.fiatCurrency, + balance.fiatSymbol)) + } + + private fun getBigDecimal(value: String, scale: Int = CurrencyFormatUtils.ETH_SCALE): BigDecimal { + return if (value.isNotEmpty()) BigDecimal(value).setScale(scale, RoundingMode.FLOOR) + else BigDecimal.ZERO + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/barcode/BarcodeCaptureActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/barcode/BarcodeCaptureActivity.java index e69b389fab3..de668ed7ba5 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/barcode/BarcodeCaptureActivity.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/barcode/BarcodeCaptureActivity.java @@ -28,15 +28,14 @@ import android.content.IntentFilter; import android.content.pm.PackageManager; import android.hardware.Camera; -import android.os.Build; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.ActivityCompat; -import android.support.v7.app.AppCompatActivity; import android.util.DisplayMetrics; import android.util.Log; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; import com.asf.wallet.R; +import com.asfoundation.wallet.ui.BaseActivity; import com.asfoundation.wallet.ui.camera.CameraSource; import com.asfoundation.wallet.ui.camera.CameraSourcePreview; import com.google.android.gms.common.ConnectionResult; @@ -47,16 +46,19 @@ import com.google.android.gms.vision.barcode.BarcodeDetector; import java.io.IOException; -public final class BarcodeCaptureActivity extends AppCompatActivity - implements BarcodeTracker.BarcodeGraphicTrackerCallback { +public final class BarcodeCaptureActivity extends BaseActivity + implements BarcodeTracker.BarcodeGraphicTrackerCallback, CameraResultListener { // Constants used to pass extra data in the intent public static final String BarcodeObject = "Barcode"; + public static final String ERROR_CODE = "error"; private static final String TAG = "Barcode-reader"; // Intent request code to handle updating play services if needed. private static final int RC_HANDLE_GMS = 9001; // Permission request codes need to be < 256 private static final int RC_HANDLE_CAMERA_PERM = 2; + private static final boolean AUTO_FOCUS = true; + private static final boolean USE_FLASH = false; private CameraSource mCameraSource; private CameraSourcePreview mPreview; @@ -68,15 +70,13 @@ public final class BarcodeCaptureActivity extends AppCompatActivity setContentView(R.layout.layout_barcode_capture); mPreview = findViewById(R.id.preview); - - boolean autoFocus = true; - boolean useFlash = false; + mPreview.addCameraResultListener(this); // Check for the camera permission before accessing the camera. If the // permission is not granted yet, request permission. - int rc = ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA); - if (rc == PackageManager.PERMISSION_GRANTED) { - createCameraSource(autoFocus, useFlash); + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED) { + createCameraSource(AUTO_FOCUS, USE_FLASH); } else { requestCameraPermission(); } @@ -93,9 +93,15 @@ public final class BarcodeCaptureActivity extends AppCompatActivity } } + @Override public void onCameraError() { + Toast.makeText(this, getString(R.string.no_camera_available), Toast.LENGTH_SHORT) + .show(); + finish(); + } + @Override public void onDetectedQrCode(Barcode barcode) { + Intent intent = new Intent(); if (barcode != null) { - Intent intent = new Intent(); intent.putExtra(BarcodeObject, barcode); setResult(CommonStatusCodes.SUCCESS, intent); finish(); @@ -105,12 +111,8 @@ public final class BarcodeCaptureActivity extends AppCompatActivity // Handles the requesting of the camera permission. private void requestCameraPermission() { Log.w(TAG, "Camera permission is not granted. Requesting permission"); - final String[] permissions = new String[] { Manifest.permission.CAMERA }; - - if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) { - ActivityCompat.requestPermissions(this, permissions, RC_HANDLE_CAMERA_PERM); - } + ActivityCompat.requestPermissions(this, permissions, RC_HANDLE_CAMERA_PERM); } /** @@ -122,7 +124,8 @@ private void requestCameraPermission() { @SuppressLint("InlinedApi") private void createCameraSource(boolean autoFocus, boolean useFlash) { Context context = getApplicationContext(); - // A barcode_capture detector is created to track barcodes. An associated multi-processor instance + // A barcode_capture detector is created to track barcodes. An associated multi-processor + // instance // is set to receive the barcode_capture detection results, track the barcodes, and maintain // graphics for each barcode_capture on screen. The factory is used by the multi-processor to // create a separate tracker instance for each barcode_capture. @@ -170,10 +173,8 @@ private void requestCameraPermission() { .setRequestedFps(24.0f); // make sure that auto focus is an available option - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - builder = - builder.setFocusMode(autoFocus ? Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE : null); - } + builder = + builder.setFocusMode(autoFocus ? Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE : null); mCameraSource = builder.setFlashMode(useFlash ? Camera.Parameters.FLASH_MODE_TORCH : null) .build(); @@ -191,6 +192,7 @@ private void requestCameraPermission() { @Override protected void onResume() { super.onResume(); startCameraSource(); + sendPageViewEvent(); } /** @@ -212,36 +214,25 @@ private void requestCameraPermission() { */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode != RC_HANDLE_CAMERA_PERM) { - Log.d(TAG, "Got unexpected permission result: " + requestCode); + if (requestCode == RC_HANDLE_CAMERA_PERM) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Camera permission granted - initialize the camera source"); + // we have permission, so create the camerasource + createCameraSource(AUTO_FOCUS, USE_FLASH); + } else { + Log.e(TAG, "Permission not granted: results len = " + grantResults.length); + if (grantResults.length > 0) { + Log.e(TAG, " Result code = " + grantResults[0]); + } + DialogInterface.OnClickListener listener = (dialog, id) -> finish(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(R.string.no_camera_permission) + .setPositiveButton(R.string.ok, listener) + .show(); + } + } else { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - return; } - - if (grantResults.length != 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Log.d(TAG, "Camera permission granted - initialize the camera source"); - // we have permission, so create the camerasource - boolean autoFocus = true; - boolean useFlash = false; - createCameraSource(autoFocus, useFlash); - return; - } - - Log.e(TAG, - "Permission not granted: results len = " + grantResults.length + " Result code = " + ( - grantResults.length > 0 ? grantResults[0] : "(empty)")); - - DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - finish(); - } - }; - - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("Multitracker sample") - .setMessage(R.string.no_camera_permission) - .setPositiveButton(R.string.ok, listener) - .show(); } /** @@ -250,7 +241,8 @@ public void onClick(DialogInterface dialog, int id) { * again when the camera source is created. */ private void startCameraSource() throws SecurityException { - // check that the device has play services available. + // check that the device has play services available. int code = GoogleApiAvailability.getInstance() .isGooglePlayServicesAvailable(getApplicationContext()); if (code != ConnectionResult.SUCCESS) { diff --git a/app/src/main/java/com/asfoundation/wallet/ui/barcode/CameraResultListener.kt b/app/src/main/java/com/asfoundation/wallet/ui/barcode/CameraResultListener.kt new file mode 100644 index 00000000000..096a4dcfd02 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/barcode/CameraResultListener.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.ui.barcode + +interface CameraResultListener { + + fun onCameraError() + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/camera/CameraSource.java b/app/src/main/java/com/asfoundation/wallet/ui/camera/CameraSource.java index 239971fcb35..5c12f10ce53 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/camera/CameraSource.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/camera/CameraSource.java @@ -25,14 +25,14 @@ import android.hardware.Camera.CameraInfo; import android.os.Build; import android.os.SystemClock; -import android.support.annotation.Nullable; -import android.support.annotation.RequiresPermission; -import android.support.annotation.StringDef; import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.WindowManager; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresPermission; +import androidx.annotation.StringDef; import com.google.android.gms.common.images.Size; import com.google.android.gms.vision.Detector; import com.google.android.gms.vision.Frame; @@ -122,7 +122,7 @@ public class CameraSource { * buffer. We use byte buffers internally because this is a more efficient way to call into * native code later (avoids a potential copy). */ - private Map mBytesToByteBuffer = new HashMap<>(); + private final Map mBytesToByteBuffer = new HashMap<>(); /** * Only allow creation via the builder class. @@ -260,13 +260,8 @@ public void release() { // SurfaceTexture was introduced in Honeycomb (11), so if we are running and // old version of Android. fall back to use SurfaceView. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - mDummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME); - mCamera.setPreviewTexture(mDummySurfaceTexture); - } else { - mDummySurfaceView = new SurfaceView(mContext); - mCamera.setPreviewDisplay(mDummySurfaceView.getHolder()); - } + mDummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME); + mCamera.setPreviewTexture(mDummySurfaceTexture); mCamera.startPreview(); mProcessingThread = new Thread(mFrameProcessor); @@ -338,15 +333,13 @@ public void stop() { mCamera.setPreviewCallbackWithBuffer(null); try { // We want to be compatible back to Gingerbread, but SurfaceTexture - // wasn't introduced until Honeycomb. Since the interface cannot use a SurfaceTexture, if the - // developer wants to display a preview we must use a SurfaceHolder. If the developer doesn't + // wasn't introduced until Honeycomb. Since the interface cannot use a SurfaceTexture, + // if the + // developer wants to display a preview we must use a SurfaceHolder. If the developer + // doesn't // want to display a preview we use a SurfaceTexture if we are running at least Honeycomb. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - mCamera.setPreviewTexture(null); - } else { - mCamera.setPreviewDisplay(null); - } + mCamera.setPreviewTexture(null); } catch (Exception e) { Log.e(TAG, "Failed to clear camera preview: " + e); } @@ -863,7 +856,7 @@ public interface AutoFocusMoveCallback { */ public static class Builder { private final Detector mDetector; - private CameraSource mCameraSource = new CameraSource(); + private final CameraSource mCameraSource = new CameraSource(); /** * Creates a camera source builder with the supplied context and detector. Camera preview @@ -882,7 +875,7 @@ public Builder(Context context, Detector detector) { } /** - * Sets the requested frame rate in frames per second. If the exact requested value is not + * Sets the requested frame rate in frames per second. If the type requested value is not * not available, the best matching available value is selected. Default: 30. */ public Builder setRequestedFps(float fps) { @@ -904,7 +897,7 @@ public Builder setFlashMode(@FlashMode String mode) { } /** - * Sets the desired width and height of the camera frames in pixels. If the exact desired + * Sets the desired width and height of the camera frames in pixels. If the type desired * values are not available options, the best matching available options are selected. * Also, we try to select a preview size which corresponds to the aspect ratio of an * associated full picture size, if applicable. Default: 1024x768. @@ -950,7 +943,7 @@ public CameraSource build() { * size is null, then there is no picture size with the same aspect ratio as the preview size. */ private static class SizePair { - private Size mPreview; + private final Size mPreview; private Size mPicture; public SizePair(Camera.Size previewSize, Camera.Size pictureSize) { @@ -1055,7 +1048,7 @@ private class FrameProcessingRunnable implements Runnable { // This lock guards all of the member variables below. private final Object mLock = new Object(); private Detector mDetector; - private long mStartTimeMillis = SystemClock.elapsedRealtime(); + private final long mStartTimeMillis = SystemClock.elapsedRealtime(); private boolean mActive = true; // These pending variables hold the state associated with the new frame awaiting processing. diff --git a/app/src/main/java/com/asfoundation/wallet/ui/camera/CameraSourcePreview.java b/app/src/main/java/com/asfoundation/wallet/ui/camera/CameraSourcePreview.java index 33db75bbd1a..31b613d8476 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/camera/CameraSourcePreview.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/camera/CameraSourcePreview.java @@ -18,12 +18,13 @@ import android.Manifest; import android.content.Context; import android.content.res.Configuration; -import android.support.annotation.RequiresPermission; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.ViewGroup; +import androidx.annotation.RequiresPermission; +import com.asfoundation.wallet.ui.barcode.CameraResultListener; import com.google.android.gms.common.images.Size; import java.io.IOException; @@ -35,6 +36,7 @@ public class CameraSourcePreview extends ViewGroup { private boolean mStartRequested; private boolean mSurfaceAvailable; private CameraSource mCameraSource; + private CameraResultListener cameraResultListener; public CameraSourcePreview(Context context, AttributeSet attrs) { super(context, attrs); @@ -48,6 +50,10 @@ public CameraSourcePreview(Context context, AttributeSet attrs) { addView(mSurfaceView); } + public void addCameraResultListener(CameraResultListener cameraResultListener) { + this.cameraResultListener = cameraResultListener; + } + @RequiresPermission(Manifest.permission.CAMERA) public void start(CameraSource cameraSource) throws IOException, SecurityException { if (cameraSource == null) { @@ -162,8 +168,13 @@ private class SurfaceCallback implements SurfaceHolder.Callback { startIfReady(); } catch (SecurityException se) { Log.e(TAG, "Do not have permission to start the camera", se); + cameraResultListener.onCameraError(); } catch (IOException e) { Log.e(TAG, "Could not start camera source.", e); + cameraResultListener.onCameraError(); + } catch (Exception e) { + Log.e(TAG, "Unknown error.", e); + cameraResultListener.onCameraError(); } } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/CurrentLevelInfo.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/CurrentLevelInfo.kt new file mode 100644 index 00000000000..46322db6f00 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/CurrentLevelInfo.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.ui.gamification + +import android.graphics.drawable.Drawable + +data class CurrentLevelInfo(val planet: Drawable?, val levelColor: Int, val title: String, + val phrase: String) diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/CurrentLevelViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/CurrentLevelViewHolder.kt new file mode 100644 index 00000000000..1d68f100d3a --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/CurrentLevelViewHolder.kt @@ -0,0 +1,108 @@ +package com.asfoundation.wallet.ui.gamification + +import android.content.Context +import android.content.res.ColorStateList +import android.os.Build +import android.view.View +import com.appcoins.wallet.gamification.LevelModel +import com.asf.wallet.R +import com.asfoundation.wallet.ui.gamification.GamificationFragment.Companion.GAMIFICATION_INFO_ID +import com.asfoundation.wallet.ui.gamification.GamificationFragment.Companion.SHOW_REACHED_LEVELS_ID +import com.asfoundation.wallet.util.CurrencyFormatUtils +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.current_level_card.view.* +import kotlinx.android.synthetic.main.current_level_layout.view.* +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.DecimalFormat + + +class CurrentLevelViewHolder(itemView: View, + private val context: Context, + private val amountSpent: BigDecimal, + private val nextLevelAmount: BigDecimal?, + private val currencyFormatUtils: CurrencyFormatUtils, + private val mapper: GamificationMapper, + private val uiEventListener: PublishSubject>) : + LevelsViewHolder(itemView) { + + override fun bind(level: LevelModel) { + val progress = getProgressPercentage(level.amount) + val progressString = validateAndGetProgressString(progress) + handleSpecificLevel(level.level, progressString, level.bonus) + setProgress(progress) + handleToggleButton(level.level) + itemView.gamification_info_btn.setOnClickListener { + uiEventListener.onNext(Pair(GAMIFICATION_INFO_ID, true)) + } + } + + private fun handleSpecificLevel(level: Int, progressPercentage: String, bonus: Double) { + val currentLevelInfo = mapper.mapCurrentLevelInfo(level) + itemView.current_level_image.setImageDrawable(currentLevelInfo.planet) + setColor(currentLevelInfo.levelColor) + setText(currentLevelInfo.title, currentLevelInfo.phrase, progressPercentage, bonus) + } + + private fun handleToggleButton(level: Int) { + if (level != 0) { + itemView.toggle_button.setOnCheckedChangeListener { _, isChecked -> + uiEventListener.onNext(Pair(SHOW_REACHED_LEVELS_ID, isChecked)) + } + } else { + itemView.toggle_button.visibility = View.GONE + } + } + + private fun setColor(color: Int) { + itemView.current_level_bonus.background = mapper.getOvalBackground(color) + itemView.current_level_progress_bar.progressTintList = ColorStateList.valueOf(color) + } + + private fun setText(title: String, phrase: String, progressPercentage: String, + bonus: Double) { + itemView.current_level_title.text = title + if (nextLevelAmount != null) { + itemView.spend_amount_text.text = + context.getString(R.string.gamif_card_body, + currencyFormatUtils.formatGamificationValues(nextLevelAmount - amountSpent)) + } else { + itemView.spend_amount_text.visibility = View.INVISIBLE + } + itemView.current_level_phrase.text = phrase + val df = DecimalFormat("###.#") + itemView.current_level_bonus.text = + context.getString(R.string.gamif_bonus, df.format(bonus)) + itemView.percentage_left.text = "$progressPercentage%" + } + + private fun getProgressPercentage(levelAmount: BigDecimal): BigDecimal { + return if (nextLevelAmount != null) { + var levelRange = nextLevelAmount.subtract(levelAmount) + if (levelRange.toDouble() == 0.0) { + levelRange = BigDecimal.ONE + } + val amountSpentInLevel = amountSpent.subtract(levelAmount) + amountSpentInLevel.divide(levelRange, 2, RoundingMode.DOWN) + .multiply(BigDecimal(100)) + } else { + BigDecimal(100) + } + } + + private fun setProgress(progress: BigDecimal) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + itemView.current_level_progress_bar.setProgress(progress.toInt(), true) + } else { + itemView.current_level_progress_bar.progress = progress.toInt() + } + } + + private fun validateAndGetProgressString(progress: BigDecimal): String { + return if (progress >= BigDecimal.ZERO && progress <= BigDecimal(100.0)) { + currencyFormatUtils.formatGamificationValues(progress) + } else { + "" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationActivity.kt new file mode 100644 index 00000000000..cb56dbcc8a8 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationActivity.kt @@ -0,0 +1,116 @@ +package com.asfoundation.wallet.ui.gamification + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.appcompat.widget.Toolbar +import com.asf.wallet.R +import com.asfoundation.wallet.ui.BaseActivity +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.activity_rewards_level.* +import kotlinx.android.synthetic.main.no_network_retry_only_layout.* + +class GamificationActivity : BaseActivity(), GamificationActivityView { + + private lateinit var menu: Menu + private lateinit var presenter: GamificationActivityPresenter + private var toolbar: Toolbar? = null + private var backEnabled = true + private var onBackPressedSubject: PublishSubject? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_rewards_level) + toolbar = toolbar() + onBackPressedSubject = PublishSubject.create() + setTitle(getString(R.string.gamif_title, bonus.toString())) + presenter = + GamificationActivityPresenter(this, CompositeDisposable(), AndroidSchedulers.mainThread()) + presenter.present() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + if (backEnabled) { + onBackPressed() + } else { + onBackPressedSubject?.onNext("") + } + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_info, menu) + this.menu = menu + return super.onCreateOptionsMenu(menu) + } + + override fun retryClick() = RxView.clicks(retry_button) + + override fun loadGamificationView() { + toolbar?.menu?.removeItem(R.id.action_info) + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, GamificationFragment()) + .commit() + } + + override fun showNetworkErrorView() { + gamification_no_network.visibility = View.VISIBLE + retry_button.visibility = View.VISIBLE + retry_animation.visibility = View.GONE + fragment_container.visibility = View.GONE + } + + override fun showRetryAnimation() { + retry_button.visibility = View.INVISIBLE + retry_animation.visibility = View.VISIBLE + } + + override fun showMainView() { + fragment_container.visibility = View.VISIBLE + gamification_no_network.visibility = View.GONE + } + + override fun onDestroy() { + presenter.stop() + super.onDestroy() + } + + private val bonus: Int by lazy { + intent.getDoubleExtra(BONUS, 25.0) + .toInt() + } + + companion object { + const val BONUS = "bonus" + + @JvmStatic + fun newIntent(context: Context, bonus: Double): Intent { + return Intent(context, GamificationActivity::class.java).apply { + putExtra(BONUS, bonus) + } + } + } + + override fun backPressed()= onBackPressedSubject!! + + override fun enableBack() { + backEnabled = true + } + + override fun disableBack() { + backEnabled = false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationActivityPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationActivityPresenter.kt new file mode 100644 index 00000000000..d5d4248dbe5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationActivityPresenter.kt @@ -0,0 +1,27 @@ +package com.asfoundation.wallet.ui.gamification + +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.TimeUnit + +class GamificationActivityPresenter(private val activity: GamificationActivityView, + private val disposable: CompositeDisposable, + private val viewScheduler: Scheduler) { + + fun present() { + activity.loadGamificationView() + handleRetryClick() + } + + private fun handleRetryClick() { + disposable.add(activity.retryClick() + .observeOn(viewScheduler) + .doOnNext { activity.showRetryAnimation() } + .delay(1, TimeUnit.SECONDS) + .observeOn(viewScheduler) + .doOnNext { activity.loadGamificationView() } + .subscribe({}, { it.printStackTrace() })) + } + + fun stop() = disposable.clear() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationActivityView.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationActivityView.kt new file mode 100644 index 00000000000..64f325395a5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationActivityView.kt @@ -0,0 +1,22 @@ +package com.asfoundation.wallet.ui.gamification + +import io.reactivex.Observable + +interface GamificationActivityView { + + fun showMainView() + + fun showRetryAnimation() + + fun showNetworkErrorView() + + fun retryClick(): Observable + + fun loadGamificationView() + + fun backPressed(): Observable + + fun enableBack() + + fun disableBack() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationFragment.kt new file mode 100644 index 00000000000..6e571204727 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationFragment.kt @@ -0,0 +1,192 @@ +package com.asfoundation.wallet.ui.gamification + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import com.appcoins.wallet.gamification.LevelModel +import com.asf.wallet.R +import com.asfoundation.wallet.analytics.gamification.GamificationAnalytics +import com.asfoundation.wallet.ui.widget.MarginItemDecoration +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.bonus_updated_layout.* +import kotlinx.android.synthetic.main.fragment_gamification.* +import kotlinx.android.synthetic.main.fragment_gamification.bottom_sheet_fragment_container +import kotlinx.android.synthetic.main.gamification_info_bottom_sheet.* +import java.math.BigDecimal +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +import javax.inject.Inject + +class GamificationFragment : BasePageViewFragment(), GamificationView { + + @Inject + lateinit var interactor: GamificationInteractor + + @Inject + lateinit var analytics: GamificationAnalytics + + @Inject + lateinit var formatter: CurrencyFormatUtils + + @Inject + lateinit var mapper: GamificationMapper + private lateinit var presenter: GamificationPresenter + private lateinit var activityView: GamificationActivityView + private lateinit var levelsAdapter: LevelsAdapter + private var uiEventListener: PublishSubject>? = null + private var onBackPressedSubject: PublishSubject? = null + private lateinit var detailsBottomSheet: BottomSheetBehavior + + companion object { + const val SHOW_REACHED_LEVELS_ID = "SHOW_REACHED_LEVELS" + const val GAMIFICATION_INFO_ID = "GAMIFICATION_INFO" + } + + override fun onAttach(context: Context) { + super.onAttach(context) + require( + context is GamificationActivityView) { GamificationFragment::class.java.simpleName + " needs to be attached to a " + GamificationActivityView::class.java.simpleName } + activityView = context + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + uiEventListener = PublishSubject.create() + onBackPressedSubject = PublishSubject.create() + presenter = + GamificationPresenter(this, activityView, interactor, analytics, formatter, + CompositeDisposable(), AndroidSchedulers.mainThread(), Schedulers.io()) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_gamification, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + detailsBottomSheet = BottomSheetBehavior.from(bottom_sheet_fragment_container) + detailsBottomSheet.addBottomSheetCallback( + object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) = Unit + + override fun onSlide(bottomSheet: View, slideOffset: Float) { + if (slideOffset == 0f) bottomsheet_coordinator_container.visibility = GONE + bottomsheet_coordinator_container.background.alpha = (255 * slideOffset).toInt() + } + }) + presenter.present(savedInstanceState) + } + + override fun displayGamificationInfo(currentLevel: Int, + nextLevelAmount: BigDecimal?, + hiddenLevels: List, + shownLevels: List, + totalSpend: BigDecimal, + updateDate: Date?) { + levelsAdapter = + LevelsAdapter(context!!, hiddenLevels, shownLevels, totalSpend, currentLevel, + nextLevelAmount, formatter, + mapper, uiEventListener!!) + gamification_recycler_view.addItemDecoration( + MarginItemDecoration(resources.getDimension(R.dimen.gamification_card_margin) + .toInt())) + gamification_recycler_view.adapter = levelsAdapter + handleBonusUpdatedText(updateDate) + } + + override fun showHeaderInformation(totalSpent: String, bonusEarned: String, symbol: String) { + bonus_earned.text = getString(R.string.value_fiat, symbol, bonusEarned) + total_spend.text = getString(R.string.gamification_how_table_a2, totalSpent) + + bonus_earned_skeleton.visibility = View.INVISIBLE + total_spend_skeleton.visibility = View.INVISIBLE + bonus_earned.visibility = View.VISIBLE + total_spend.visibility = View.VISIBLE + } + + override fun getUiClick() = uiEventListener!! + + override fun toggleReachedLevels(show: Boolean) { + levelsAdapter.toggleReachedLevels(show) + gamification_scroll_view.scrollTo(0, 0) + } + + private fun handleBonusUpdatedText(updateDate: Date?) { + if (updateDate != null) { + val df: DateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + val date = df.format(updateDate) + bonus_update_text.text = getString(R.string.pioneer_bonus_updated_body, date) + bonus_update.visibility = View.VISIBLE + } + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + override fun getHomeBackPressed() = activityView.backPressed() + + override fun handleBackPressed() { + // Currently we only call the hide bottom sheet + // but maybe later additional stuff needs to be handled + updateBottomSheetVisibility() + } + + override fun getBottomSheetButtonClick() = RxView.clicks(got_it_button) + + override fun getBackPressed() = onBackPressedSubject!! + + override fun updateBottomSheetVisibility() { + if (detailsBottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) { + detailsBottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + disableBackListener(bottomsheet_coordinator_container) + } else { + detailsBottomSheet.state = BottomSheetBehavior.STATE_EXPANDED + bottomsheet_coordinator_container.visibility = VISIBLE + bottomsheet_coordinator_container.background.alpha = 255 + setBackListener(bottomsheet_coordinator_container) + } + } + + override fun getBottomSheetContainerClick() = RxView.clicks(bottomsheet_coordinator_container) + + private fun setBackListener(view: View) { + activityView.disableBack() + view.apply { + isFocusableInTouchMode = true + requestFocus() + setOnKeyListener { _, keyCode, keyEvent -> + if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) { + if (detailsBottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) + onBackPressedSubject?.onNext("") + } + true + } + } + } + + private fun disableBackListener(view: View) { + activityView.enableBack() + view.apply { + isFocusableInTouchMode = false + setOnKeyListener(null) + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationInfo.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationInfo.kt new file mode 100644 index 00000000000..28b75c4d93d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationInfo.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.ui.gamification + +import com.appcoins.wallet.gamification.repository.Levels +import java.math.BigDecimal +import java.util.* + +data class GamificationInfo(val currentLevel: Int, val totalSpend: BigDecimal, + val nextLevelAmount: BigDecimal?, + val levels: List, + val updateDate: Date?, + val status: Status) { + + constructor(status: Status) : this(0, BigDecimal.ZERO, null, emptyList(), null, status) + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationInteractor.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationInteractor.kt new file mode 100644 index 00000000000..552837aa388 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationInteractor.kt @@ -0,0 +1,102 @@ +package com.asfoundation.wallet.ui.gamification + +import com.appcoins.wallet.gamification.Gamification +import com.appcoins.wallet.gamification.GamificationScreen +import com.appcoins.wallet.gamification.repository.ForecastBonus +import com.appcoins.wallet.gamification.repository.ForecastBonusAndLevel +import com.appcoins.wallet.gamification.repository.GamificationStats +import com.appcoins.wallet.gamification.repository.Levels +import com.appcoins.wallet.gamification.repository.entity.GamificationResponse +import com.appcoins.wallet.gamification.repository.entity.PromotionsResponse +import com.asfoundation.wallet.entity.Wallet +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.service.LocalCurrencyConversionService +import com.asfoundation.wallet.ui.iab.FiatValue +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.functions.Function3 +import java.math.BigDecimal + +class GamificationInteractor( + private val gamification: Gamification, + private val defaultWallet: FindDefaultWalletInteract, + private val conversionService: LocalCurrencyConversionService) { + + private var isBonusActiveAndValid: Boolean = false + + fun getLevels(): Single { + return defaultWallet.find() + .flatMap { gamification.getLevels(it.address) } + } + + fun getUserStats(): Single { + return defaultWallet.find() + .flatMap { gamification.getUserStats(it.address) } + } + + fun getEarningBonus(packageName: String, amount: BigDecimal): Single { + return defaultWallet.find() + .flatMap { wallet: Wallet -> + Single.zip( + gamification.getEarningBonus(wallet.address, packageName, amount), + conversionService.localCurrency, + gamification.getUserBonusAndLevel(wallet.address), + Function3 { appcBonusValue: ForecastBonus, localCurrency: FiatValue, userBonusAndLevel: ForecastBonusAndLevel -> + map(appcBonusValue, localCurrency, userBonusAndLevel, amount) + }) + } + .doOnSuccess { isBonusActiveAndValid = isBonusActiveAndValid(it) } + } + + + private fun map(forecastBonus: ForecastBonus, fiatValue: FiatValue, + forecastBonusAndLevel: ForecastBonusAndLevel, + amount: BigDecimal): ForecastBonusAndLevel { + val status = getBonusStatus(forecastBonus, forecastBonusAndLevel) + var bonus = forecastBonus.amount.multiply(fiatValue.amount) + + if (amount.multiply(fiatValue.amount) >= forecastBonusAndLevel.minAmount) { + bonus = bonus.add(forecastBonusAndLevel.amount) + } + return ForecastBonusAndLevel(status, bonus, fiatValue.symbol, + level = forecastBonusAndLevel.level) + } + + private fun getBonusStatus(forecastBonus: ForecastBonus, + userBonusAndLevel: ForecastBonusAndLevel): ForecastBonus.Status { + return if (forecastBonus.status == ForecastBonus.Status.ACTIVE || userBonusAndLevel.status == ForecastBonus.Status.ACTIVE) { + ForecastBonus.Status.ACTIVE + } else { + ForecastBonus.Status.INACTIVE + } + } + + fun hasNewLevel(walletAddress: String, + gamificationResponse: GamificationResponse?, + screen: GamificationScreen): Single { + return if (gamificationResponse == null || gamificationResponse.status != PromotionsResponse.Status.ACTIVE) { + Single.just(false) + } else { + gamification.hasNewLevel(walletAddress, screen.toString()) + } + } + + fun levelShown(level: Int, screen: GamificationScreen): Completable { + return defaultWallet.find() + .flatMapCompletable { gamification.levelShown(it.address, level, screen.toString()) } + } + + fun getAppcToLocalFiat(value: String, scale: Int): Observable { + return conversionService.getAppcToLocalFiat(value, scale) + .onErrorReturn { FiatValue(BigDecimal("-1"), "", "") } + } + + fun isBonusActiveAndValid(): Boolean { + return isBonusActiveAndValid + } + + fun isBonusActiveAndValid(forecastBonus: ForecastBonusAndLevel): Boolean { + return forecastBonus.status == ForecastBonus.Status.ACTIVE && forecastBonus.amount > BigDecimal.ZERO + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationMapper.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationMapper.kt new file mode 100644 index 00000000000..a8b33eacd98 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationMapper.kt @@ -0,0 +1,159 @@ +package com.asfoundation.wallet.ui.gamification + +import android.content.Context +import android.graphics.drawable.Drawable +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.DrawableCompat +import com.asf.wallet.R + +class GamificationMapper(private val context: Context) { + + fun mapCurrentLevelInfo(level: Int): CurrentLevelInfo { + return when (level) { + 0 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_earth), + getColor(R.color.gamification_light_green), + getString(R.string.gamif_card_start), + getString(R.string.gamif_quote_earth)) + 1 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_moon), + getColor(R.color.gamification_light_grey), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_moon), + getString(R.string.gamif_quote_moon)) + 2 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_mars), + getColor(R.color.gamification_red), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_mars), + getString(R.string.gamif_quote_mars)) + 3 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_phobos), + getColor(R.color.gamification_blue_grey), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_phobos), + getString(R.string.gamif_quote_phobos)) + 4 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_jupiter), + getColor(R.color.gamification_orange), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_jupiter), + getString(R.string.gamif_quote_jupiter)) + 5 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_europa), + getColor(R.color.gamification_dark_yellow), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_europa), + getString(R.string.gamif_quote_europa)) + 6 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_saturn), + getColor(R.color.gamification_yellow), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_saturn), + getString(R.string.gamif_quote_saturn)) + 7 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_titan), + getColor(R.color.gamification_blue_green), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_titan), + getString(R.string.gamif_quote_titan)) + 8 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_uranus), + getColor(R.color.gamification_old_blue), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_uranus), + getString(R.string.gamif_quote_uranus)) + 9 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_neptune), + getColor(R.color.gamification_blue), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_neptune), + getString(R.string.gamif_quote_neptune)) + 10 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_unknown_planet_purple), + getColor(R.color.gamification_purple), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_unknown), + getString(R.string.gamif_quote_planetx)) + 11 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_unknown_planet_green), + getColor(R.color.gamification_green), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_unknown), + getString(R.string.gamif_quote_planetx)) + 12 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_unknown_planet_brown), + getColor(R.color.gamification_brown), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_unknown), + getString(R.string.gamif_quote_planetx)) + 13 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_unknown_planet_blue), + getColor(R.color.gamification_light_blue), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_unknown), + getString(R.string.gamif_quote_planetx)) + 14 -> CurrentLevelInfo(getDrawable(R.drawable.gamification_unknown_planet_red), + getColor(R.color.gamification_dark_red), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_unknown), + getString(R.string.gamif_quote_planetx)) + else -> CurrentLevelInfo(getDrawable(R.drawable.gamification_unknown_planet_purple), + getColor(R.color.gamification_purple), + getFullString(R.string.gamif_card_title, R.string.gamif_placeholder_unknown), + getString(R.string.gamif_quote_planetx)) + } + } + + fun mapReachedLevelInfo(level: Int): ReachedLevelInfo { + return when (level) { + 0 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_earth_reached), + getString(R.string.gamif_achievement_start), + getString(R.string.gamif_achievement_start_sub)) + 1 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_moon_reached), + getFullString(R.string.gamif_achievement_reach, R.string.gamif_placeholder_moon), + getString(R.string.gamif_distance_moon)) + 2 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_mars_reached), + getFullString(R.string.gamif_achievement_reach, R.string.gamif_placeholder_mars), + getString(R.string.gamif_distance_mars)) + 3 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_phobos_reached), + getFullString(R.string.gamif_achievement_reach, R.string.gamif_placeholder_phobos), + getString(R.string.gamif_distance_phobos)) + 4 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_jupiter_reached), + getFullString(R.string.gamif_achievement_reach, R.string.gamif_placeholder_jupiter), + getString(R.string.gamif_distance_jupiter)) + 5 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_europa_reached), + getFullString(R.string.gamif_achievement_reach, R.string.gamif_placeholder_europa), + getString(R.string.gamif_distance_europa)) + 6 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_saturn_reached), + getFullString(R.string.gamif_achievement_reach, R.string.gamif_placeholder_saturn), + getString(R.string.gamif_distance_saturn)) + 7 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_titan_reached), + getFullString(R.string.gamif_achievement_reach, R.string.gamif_placeholder_titan), + getString(R.string.gamif_distance_titan)) + 8 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_uranus_reached), + getFullString(R.string.gamif_achievement_reach, R.string.gamif_placeholder_uranus), + getString(R.string.gamif_distance_uranus)) + 9 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_neptune_reached), + getFullString(R.string.gamif_achievement_reach, R.string.gamif_placeholder_neptune), + getString(R.string.gamif_distance_neptune)) + 10 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_unknown_planet_purple_reached), + getUnknownPlanetString(10), + getString(R.string.gamif_distance_unkown)) + 11 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_unknown_planet_green_reached), + getUnknownPlanetString(11), + getString(R.string.gamif_distance_unkown)) + 12 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_unknown_planet_brown_reached), + getUnknownPlanetString(12), + getString(R.string.gamif_distance_unkown)) + 13 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_unknown_planet_blue_reached), + getUnknownPlanetString(13), + getString(R.string.gamif_distance_unkown)) + 14 -> ReachedLevelInfo(getDrawable(R.drawable.gamification_unknown_planet_red_reached), + getUnknownPlanetString(14), + getString(R.string.gamif_distance_unkown)) + else -> ReachedLevelInfo(getDrawable(R.drawable.gamification_unknown_planet_purple_reached), + getUnknownPlanetString(15), + getString(R.string.gamif_distance_unkown)) + } + } + + fun getOvalBackground(levelColor: Int): Drawable? { + val ovalBackground = + ResourcesCompat.getDrawable(context.resources, R.drawable.oval_grey_background, null) + ovalBackground?.let { drawable -> + DrawableCompat.setTint(drawable.mutate(), levelColor) + } + return ovalBackground + } + + private fun getDrawable(@DrawableRes drawable: Int) = + ResourcesCompat.getDrawable(context.resources, drawable, null) + + private fun getColor(@ColorRes color: Int) = + ResourcesCompat.getColor(context.resources, color, null) + + private fun getString(@StringRes string: Int) = context.getString(string) + + private fun getFullString(@StringRes string: Int, @StringRes planet: Int) = + context.getString(string, getString(planet)) + + private fun getUnknownPlanetString(number: Int) = + context.getString(R.string.gamif_achievement_reach, + context.getString(R.string.gamif_placeholder_planetx, number.toString())) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationPresenter.kt new file mode 100644 index 00000000000..f5d68e86269 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationPresenter.kt @@ -0,0 +1,147 @@ +package com.asfoundation.wallet.ui.gamification + +import android.os.Bundle +import com.appcoins.wallet.gamification.GamificationScreen +import com.appcoins.wallet.gamification.LevelModel +import com.appcoins.wallet.gamification.LevelModel.LevelType +import com.appcoins.wallet.gamification.repository.GamificationStats +import com.appcoins.wallet.gamification.repository.Levels +import com.asfoundation.wallet.analytics.gamification.GamificationAnalytics +import com.asfoundation.wallet.ui.gamification.GamificationFragment.Companion.GAMIFICATION_INFO_ID +import com.asfoundation.wallet.ui.gamification.GamificationFragment.Companion.SHOW_REACHED_LEVELS_ID +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.asfoundation.wallet.util.isNoNetworkException +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.BiFunction +import java.math.BigDecimal + +class GamificationPresenter(private val view: GamificationView, + private val activityView: GamificationActivityView, + private val gamification: GamificationInteractor, + private val analytics: GamificationAnalytics, + private val formatter: CurrencyFormatUtils, + private val disposables: CompositeDisposable, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler) { + + fun present(savedInstanceState: Bundle?) { + handleLevelInformation(savedInstanceState == null) + handleLevelsClick() + handleBottomSheetVisibility() + handleBackPress() + } + + private fun handleLevelsClick() { + disposables.add(view.getUiClick() + .doOnNext { + when (it.first) { + SHOW_REACHED_LEVELS_ID -> view.toggleReachedLevels(it.second) + GAMIFICATION_INFO_ID -> view.updateBottomSheetVisibility() + } + } + .subscribe()) + } + + private fun handleLevelInformation(sendEvent: Boolean) { + disposables.add(Single.zip(gamification.getLevels(), gamification.getUserStats(), + BiFunction { levels: Levels, gamificationStats: GamificationStats -> + handleHeaderInformation(gamificationStats.totalEarned, gamificationStats.totalSpend, + gamificationStats.status) + mapToGamificationInfo(levels, gamificationStats) + }) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { displayInformation(it, sendEvent) } + .flatMapCompletable { + gamification.levelShown(it.currentLevel, GamificationScreen.MY_LEVEL) + } + .subscribe({}, { handleError(it) })) + } + + private fun mapToGamificationInfo(levels: Levels, + gamificationStats: GamificationStats): GamificationInfo { + var status = Status.UNKNOWN_ERROR + if (levels.status == Levels.Status.OK && gamificationStats.status == GamificationStats.Status.OK) { + return GamificationInfo(gamificationStats.level, gamificationStats.totalSpend, + gamificationStats.nextLevelAmount, levels.list, levels.updateDate, + Status.OK) + } + if (levels.status == Levels.Status.NO_NETWORK || gamificationStats.status == GamificationStats.Status.NO_NETWORK) { + status = Status.NO_NETWORK + } + return GamificationInfo(status) + } + + private fun displayInformation(gamification: GamificationInfo, sendEvent: Boolean) { + if (gamification.status != Status.OK) { + activityView.showNetworkErrorView() + } else { + val currentLevel = gamification.currentLevel + activityView.showMainView() + val levels = map(gamification.levels, currentLevel) + view.displayGamificationInfo(currentLevel, gamification.nextLevelAmount, levels.first, + levels.second, gamification.totalSpend, gamification.updateDate) + if (sendEvent) analytics.sendMainScreenViewEvent(currentLevel + 1) + } + } + + private fun map(levels: List, + currentLevel: Int): Pair, List> { + val hiddenList = ArrayList() + val shownList = ArrayList() + for (level in levels) { + val viewType = when { + level.level < currentLevel -> LevelType.REACHED + level.level == currentLevel -> LevelType.CURRENT + else -> LevelType.UNREACHED + } + val levelViewModel = LevelModel(level.amount, level.bonus, level.level, viewType) + if (viewType == LevelType.REACHED) hiddenList.add(levelViewModel) + else shownList.add(levelViewModel) + } + return Pair(hiddenList, shownList) + } + + private fun handleHeaderInformation(totalEarned: BigDecimal, totalSpend: BigDecimal, + status: GamificationStats.Status) { + if (status == GamificationStats.Status.OK) { + disposables.add(gamification.getAppcToLocalFiat(totalEarned.toString(), 2) + .filter { it.amount.toInt() >= 0 } + .observeOn(viewScheduler) + .doOnNext { + val totalSpent = formatter.formatCurrency(totalSpend, WalletCurrency.FIAT) + val bonusEarned = formatter.formatCurrency(it.amount, WalletCurrency.FIAT) + view.showHeaderInformation(totalSpent, bonusEarned, it.symbol) + } + .subscribeOn(networkScheduler) + .subscribe({}, { handleError(it) })) + } + } + + private fun handleError(throwable: Throwable) { + throwable.printStackTrace() + if (throwable.isNoNetworkException()) { + activityView.showNetworkErrorView() + } + } + + fun stop() = disposables.clear() + + private fun handleBottomSheetVisibility() { + disposables.add(view.getBottomSheetButtonClick().mergeWith(view.getBottomSheetContainerClick()) + .observeOn(viewScheduler) + .doOnNext { view.updateBottomSheetVisibility() } + .subscribe({}, { handleError(it) })) + } + + private fun handleBackPress() { + disposables.add(Observable.merge(view.getBackPressed(), view.getHomeBackPressed()) + .observeOn(viewScheduler) + .doOnNext { view.handleBackPressed() } + .subscribe({}, { it.printStackTrace() })) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationView.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationView.kt new file mode 100644 index 00000000000..5e3d14bbc92 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/GamificationView.kt @@ -0,0 +1,33 @@ +package com.asfoundation.wallet.ui.gamification + +import com.appcoins.wallet.gamification.LevelModel +import io.reactivex.Observable +import java.math.BigDecimal +import java.util.* + +interface GamificationView { + + fun displayGamificationInfo(currentLevel: Int, nextLevelAmount: BigDecimal?, + hiddenLevels: List, + shownLevels: List, + totalSpend: BigDecimal, + updateDate: Date?) + + fun showHeaderInformation(totalSpent: String, bonusEarned: String, symbol: String) + + fun getUiClick(): Observable> + + fun toggleReachedLevels(show: Boolean) + + fun getHomeBackPressed(): Observable + + fun handleBackPressed() + + fun getBottomSheetButtonClick(): Observable + + fun getBackPressed(): Observable + + fun updateBottomSheetVisibility() + + fun getBottomSheetContainerClick(): Observable +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/LevelReachedViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/LevelReachedViewHolder.kt new file mode 100644 index 00000000000..34f6a26166c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/LevelReachedViewHolder.kt @@ -0,0 +1,16 @@ +package com.asfoundation.wallet.ui.gamification + +import android.view.View +import com.appcoins.wallet.gamification.LevelModel +import kotlinx.android.synthetic.main.reached_level_layout.view.* + +class LevelReachedViewHolder(itemView: View, private val mapper: GamificationMapper) : + LevelsViewHolder(itemView) { + + override fun bind(level: LevelModel) { + val reachedLevelInfo = mapper.mapReachedLevelInfo(level.level) + itemView.level_icon.setImageDrawable(reachedLevelInfo.planet) + itemView.level_title.text = reachedLevelInfo.reachedTitle + itemView.level_description.text = reachedLevelInfo.reachedSubtitle + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/LevelsAdapter.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/LevelsAdapter.kt new file mode 100644 index 00000000000..87b3d378d5d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/LevelsAdapter.kt @@ -0,0 +1,82 @@ +package com.asfoundation.wallet.ui.gamification + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.appcoins.wallet.gamification.LevelModel +import com.appcoins.wallet.gamification.LevelModel.LevelType +import com.asf.wallet.R +import com.asfoundation.wallet.promotions.PromotionClick +import com.asfoundation.wallet.util.CurrencyFormatUtils +import io.reactivex.subjects.PublishSubject +import java.math.BigDecimal + +class LevelsAdapter(private val context: Context, + private val hiddenLevels: List, + shownLevels: List, + private val amountSpent: BigDecimal, private val currentLevel: Int, + private val nextLevelAmount: BigDecimal?, + private val currencyFormatUtils: CurrencyFormatUtils, + private val mapper: GamificationMapper, + private val uiEventListener: PublishSubject>) : + RecyclerView.Adapter() { + + private var activeLevelList: MutableList = shownLevels.toMutableList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LevelsViewHolder { + return when (viewType) { + REACHED_VIEW_TYPE -> { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.reached_level_layout, parent, false) + LevelReachedViewHolder(layout, mapper) + } + CURRENT_LEVEL_VIEW_TYPE -> { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.current_level_layout, parent, false) + CurrentLevelViewHolder(layout, context, amountSpent, nextLevelAmount, currencyFormatUtils, + mapper, uiEventListener) + } + else -> { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.unreached_level_layout, parent, false) + UnreachedLevelViewHolder(layout, context, currencyFormatUtils) + } + } + } + + override fun getItemCount() = activeLevelList.size + + override fun onBindViewHolder(holder: LevelsViewHolder, position: Int) { + holder.bind(activeLevelList[position]) + } + + override fun getItemViewType(position: Int): Int { + return when (activeLevelList[position].levelType) { + LevelType.REACHED -> REACHED_VIEW_TYPE + LevelType.CURRENT -> CURRENT_LEVEL_VIEW_TYPE + LevelType.UNREACHED -> UNREACHED_VIEW_TYPE + } + } + + fun toggleReachedLevels(show: Boolean) { + if (show) { + if (currentLevel != 0) { + for (level in hiddenLevels.reversed()) { + activeLevelList.add(0, level) + } + notifyItemRangeInserted(0, currentLevel) + } + } else { + activeLevelList.removeAll { it.levelType == LevelType.REACHED } + notifyDataSetChanged() + } + } + + companion object { + private const val CURRENT_LEVEL_VIEW_TYPE = 0 + private const val REACHED_VIEW_TYPE = 1 + private const val UNREACHED_VIEW_TYPE = 2 + } +} + diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/LevelsViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/LevelsViewHolder.kt new file mode 100644 index 00000000000..ce6568f82f7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/LevelsViewHolder.kt @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.ui.gamification + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.appcoins.wallet.gamification.LevelModel + +abstract class LevelsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + abstract fun bind(level: LevelModel) +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/ProgressAnimation.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/ProgressAnimation.kt new file mode 100644 index 00000000000..1fbe30423c9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/ProgressAnimation.kt @@ -0,0 +1,16 @@ +package com.asfoundation.wallet.ui.gamification + +import android.view.animation.Animation +import android.view.animation.Transformation +import android.widget.ProgressBar + +class ProgressAnimation(private val progressBar: ProgressBar?, private val from: Float, + private val to: Float) : Animation() { + + override fun applyTransformation(interpolatedTime: Float, t: Transformation) { + super.applyTransformation(interpolatedTime, t) + val value = from + (to - from) * interpolatedTime + progressBar?.progress = value.toInt() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/ReachedLevelInfo.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/ReachedLevelInfo.kt new file mode 100644 index 00000000000..40ca3eb6ec5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/ReachedLevelInfo.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.ui.gamification + +import android.graphics.drawable.Drawable + +data class ReachedLevelInfo(val planet: Drawable?, val reachedTitle: String, + val reachedSubtitle: String) diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/SharedPreferencesGamificationLocalData.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/SharedPreferencesGamificationLocalData.kt new file mode 100644 index 00000000000..8da865e4bc5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/SharedPreferencesGamificationLocalData.kt @@ -0,0 +1,162 @@ +package com.asfoundation.wallet.ui.gamification + +import android.content.SharedPreferences +import com.appcoins.wallet.gamification.GamificationScreen +import com.appcoins.wallet.gamification.repository.GamificationLocalData +import com.appcoins.wallet.gamification.repository.LevelDao +import com.appcoins.wallet.gamification.repository.LevelsDao +import com.appcoins.wallet.gamification.repository.PromotionDao +import com.appcoins.wallet.gamification.repository.entity.* +import com.asfoundation.wallet.promotions.PromotionsInteractor.Companion.GAMIFICATION_ID +import com.asfoundation.wallet.promotions.PromotionsInteractor.Companion.REFERRAL_ID +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.functions.BiFunction +import java.util.concurrent.TimeUnit + +class SharedPreferencesGamificationLocalData(private val preferences: SharedPreferences, + private val promotionDao: PromotionDao, + private val levelsDao: LevelsDao, + private val levelDao: LevelDao) : + GamificationLocalData { + + companion object { + private const val SHOWN_LEVEL = "shown_level" + private const val SHOWN_GENERIC = "shown_generic" + private const val SCREEN = "screen_" + private const val ID = "id_" + private const val GAMIFICATION_LEVEL = "gamification_level" + } + + override fun getLastShownLevel(wallet: String, screen: String): Single { + return Single.fromCallable { preferences.getInt(getKey(wallet, screen), -1) } + } + + override fun saveShownLevel(wallet: String, level: Int, screen: String) { + return preferences.edit() + .putInt(getKey(wallet, screen), level) + .apply() + } + + override fun getSeenGenericPromotion(id: String, screen: String): Boolean { + return preferences.getBoolean(getKeyGeneric(screen, id), false) + } + + override fun setSeenGenericPromotion(id: String, screen: String) { + return preferences.edit() + .putBoolean(getKeyGeneric(screen, id), true) + .apply() + } + + override fun setGamificationLevel(gamificationLevel: Int): Completable { + return Completable.fromCallable { + preferences.edit() + .putInt(GAMIFICATION_LEVEL, gamificationLevel) + .apply() + } + } + + + private fun getKey(wallet: String, screen: String): String { + return if (screen == GamificationScreen.MY_LEVEL.toString()) { + SHOWN_LEVEL + wallet + } else { + SHOWN_LEVEL + wallet + SCREEN + screen + } + } + + private fun getKeyGeneric(screen: String, id: String) = + SHOWN_GENERIC + SCREEN + screen + ID + id + + override fun getPromotions(): Single> { + return promotionDao.getPromotions() + .map { filterByDate(it) } + .map { mapToPromotionResponse(it) } + } + + private fun filterByDate(promotions: List): List { + val currentTime = TimeUnit.SECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS) + return promotions.filter { it.endDate == null || currentTime < it.endDate!! } + } + + private fun mapToPromotionResponse(promotions: List): List { + return promotions.map { + when (it.id) { + GAMIFICATION_ID -> + GamificationResponse(it.id, it.priority, it.bonus!!, it.totalSpend!!, it.totalEarned!!, + it.level!!, it.nextLevelAmount, it.status!!, it.bundle!!) + REFERRAL_ID -> ReferralResponse(it.id, it.priority, it.maxAmount!!, it.available!!, + it.bundle!!, it.completed!!, it.currency!!, it.symbol!!, it.invited!!, it.link, + it.pendingAmount!!, it.receivedAmount!!, it.userStatus, it.minAmount!!, it.status!!, + it.amount!!) + else -> + GenericResponse(it.id, it.priority, it.currentProgress, it.description!!, it.endDate!!, + it.icon, + it.linkedPromotionId, it.objectiveProgress, it.startDate, it.title!!, it.viewType!!, + it.detailsLink) + } + } + } + + override fun deletePromotions() = promotionDao.deletePromotions() + + override fun insertPromotions(promotions: List): Completable { + return Single.create> { emitter -> + val results: List = promotions.map { + when (it) { + is GamificationResponse -> + PromotionEntity(it.id, it.priority, bonus = it.bonus, totalSpend = it.totalSpend, + totalEarned = it.totalEarned, level = it.level, + nextLevelAmount = it.nextLevelAmount, status = it.status, bundle = it.bundle) + is ReferralResponse -> { + PromotionEntity(it.id, it.priority, maxAmount = it.maxAmount, available = it.available, + bundle = it.bundle, completed = it.completed, currency = it.currency, + symbol = it.symbol, invited = it.invited, link = it.link, + pendingAmount = it.pendingAmount, receivedAmount = it.receivedAmount, + userStatus = it.userStatus, minAmount = it.minAmount, status = it.status, + amount = it.amount) + } + else -> { + val genericResponse = it as GenericResponse + PromotionEntity(genericResponse.id, genericResponse.priority, + currentProgress = genericResponse.currentProgress, + description = genericResponse.description, endDate = genericResponse.endDate, + icon = genericResponse.icon, linkedPromotionId = genericResponse.linkedPromotionId, + objectiveProgress = genericResponse.objectiveProgress, + startDate = genericResponse.startDate, title = genericResponse.title, + viewType = genericResponse.viewType, detailsLink = genericResponse.detailsLink) + } + } + } + emitter.onSuccess(results) + } + .flatMapCompletable { promotionDao.insertPromotions(it) } + } + + override fun deleteLevels(): Completable { + return levelDao.deleteLevels() + .andThen(levelsDao.deleteLevels()) + } + + override fun getLevels(): Single { + return Single.zip( + levelDao.getLevels(), + levelsDao.getLevels(), + BiFunction { levelList, levels -> mapToLevelsResponse(levelList, levels) } + ) + } + + override fun insertLevels(levels: LevelsResponse): Completable { + val levelsEntity = LevelsEntity(null, levels.status, levels.updateDate) + val levelEntityList = + levels.list.map { LevelEntity(null, it.amount, it.bonus, it.level) } + return levelsDao.insertLevels(levelsEntity) + .andThen(levelDao.insertLevels(levelEntityList)) + } + + private fun mapToLevelsResponse(levelEntity: List, + levelsEntity: LevelsEntity): LevelsResponse { + val levels = levelEntity.map { Level(it.amount, it.bonus, it.level) } + return LevelsResponse(levels, levelsEntity.status, levelsEntity.updateDate) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/UnreachedLevelViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/UnreachedLevelViewHolder.kt new file mode 100644 index 00000000000..afa8f1853e9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/UnreachedLevelViewHolder.kt @@ -0,0 +1,21 @@ +package com.asfoundation.wallet.ui.gamification + +import android.content.Context +import android.view.View +import com.appcoins.wallet.gamification.LevelModel +import com.asf.wallet.R +import com.asfoundation.wallet.util.CurrencyFormatUtils +import kotlinx.android.synthetic.main.unreached_level_layout.view.* +import java.text.DecimalFormat + +class UnreachedLevelViewHolder(itemView: View, private val context: Context, + private val currencyFormatUtils: CurrencyFormatUtils) : + LevelsViewHolder(itemView) { + + override fun bind(level: LevelModel) { + itemView.locked_text.text = context.getString(R.string.gamif_next_goals, + currencyFormatUtils.formatGamificationValues(level.amount)) + val df = DecimalFormat("###.#") + itemView.locked_bonus.text = context.getString(R.string.gamif_bonus, df.format(level.bonus)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/gamification/UserRewardsStatus.kt b/app/src/main/java/com/asfoundation/wallet/ui/gamification/UserRewardsStatus.kt new file mode 100644 index 00000000000..a6d118345eb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/gamification/UserRewardsStatus.kt @@ -0,0 +1,5 @@ +package com.asfoundation.wallet.ui.gamification + +enum class Status { + OK, NO_NETWORK, UNKNOWN_ERROR +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/AppCoinsOperation.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppCoinsOperation.java index 6fa6f9b3aac..4d3520fea2d 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/iab/AppCoinsOperation.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppCoinsOperation.java @@ -1,6 +1,6 @@ package com.asfoundation.wallet.ui.iab; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; public class AppCoinsOperation { private final String transactionId; diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/AppCoinsOperationRepository.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppCoinsOperationRepository.java index 2f075cea7ae..a226d697538 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/iab/AppCoinsOperationRepository.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppCoinsOperationRepository.java @@ -1,6 +1,6 @@ package com.asfoundation.wallet.ui.iab; -import com.asfoundation.wallet.repository.Repository; +import com.appcoins.wallet.commons.Repository; import com.asfoundation.wallet.ui.iab.database.AppCoinsOperationDao; import io.reactivex.Completable; import io.reactivex.Observable; @@ -30,7 +30,7 @@ public AppCoinsOperationRepository(AppCoinsOperationDao inAppPurchaseDataDao, @Override public Observable get(String key) { return inAppPurchaseDataDao.getAsFlowable(key) .toObservable() - .map(appCoinsOperationEntity -> mapper.map(appCoinsOperationEntity)); + .map(mapper::map); } @Override public Completable remove(String key) { diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/AppCoinsPaymentMethod.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppCoinsPaymentMethod.kt new file mode 100644 index 00000000000..3e4b3efca08 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppCoinsPaymentMethod.kt @@ -0,0 +1,12 @@ +package com.asfoundation.wallet.ui.iab + +data class AppCoinsPaymentMethod(override val id: String, override val label: String, + override val iconUrl: String, + override val isEnabled: Boolean = false, + val isAppcEnabled: Boolean = false, + val isCreditsEnabled: Boolean = false, val appcLabel: String, + val creditsLabel: String, val creditsIconUrl: String, + override var disabledReason: Int? = null, + val disabledReasonAppc: Int? = null, + val disabledReasonCredits: Int? = null) : + PaymentMethod(id, label, iconUrl, null, isEnabled) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsOperationsDataSaver.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsOperationsDataSaver.java index 9d73fe13220..8221bb0d28e 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsOperationsDataSaver.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsOperationsDataSaver.java @@ -1,6 +1,6 @@ package com.asfoundation.wallet.ui.iab; -import com.asfoundation.wallet.repository.Repository; +import com.appcoins.wallet.commons.Repository; import io.reactivex.Observable; import io.reactivex.ObservableSource; import io.reactivex.Scheduler; @@ -34,7 +34,7 @@ public void start() { } return list; }) - .subscribeOn(scheduler) + .observeOn(scheduler) .toObservable() .flatMap(Observable::merge) .flatMap( diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsRewardsBuyFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsRewardsBuyFragment.kt new file mode 100644 index 00000000000..6b0a511495b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsRewardsBuyFragment.kt @@ -0,0 +1,216 @@ +package com.asfoundation.wallet.ui.iab + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import com.appcoins.wallet.bdsbilling.repository.entity.Purchase +import com.appcoins.wallet.billing.BillingMessagesMapper +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.TransferParser +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.fragment_iab_transaction_completed.* +import kotlinx.android.synthetic.main.iab_error_layout.* +import kotlinx.android.synthetic.main.iab_error_layout.generic_error_layout +import kotlinx.android.synthetic.main.reward_payment_layout.* +import kotlinx.android.synthetic.main.support_error_layout.* +import java.math.BigDecimal +import javax.inject.Inject + + +class AppcoinsRewardsBuyFragment : BasePageViewFragment(), AppcoinsRewardsBuyView { + + @Inject + lateinit var rewardsManager: RewardsManager + + @Inject + lateinit var transferParser: TransferParser + + @Inject + lateinit var billingMessagesMapper: BillingMessagesMapper + + @Inject + lateinit var analytics: BillingAnalytics + + @Inject + lateinit var formatter: CurrencyFormatUtils + + @Inject + lateinit var appcoinsRewardsBuyInteract: AppcoinsRewardsBuyInteract + + @Inject + lateinit var logger: Logger + + private lateinit var presenter: AppcoinsRewardsBuyPresenter + private lateinit var iabView: IabView + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.reward_payment_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter = AppcoinsRewardsBuyPresenter(this, rewardsManager, AndroidSchedulers.mainThread(), + Schedulers.io(), CompositeDisposable(), amount, uri, transactionBuilder.domain, + transferParser, isBds, analytics, transactionBuilder, formatter, gamificationLevel, + appcoinsRewardsBuyInteract, logger) + setupTransactionCompleteAnimation() + presenter.present() + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + override fun finish(purchase: Purchase) = finish(purchase, null) + + override fun showLoading() { + generic_error_layout.visibility = View.GONE + iab_activity_transaction_completed.visibility = View.INVISIBLE + loading_view.visibility = View.VISIBLE + } + + override fun hideLoading() { + loading_view.visibility = View.GONE + } + + override fun showNoNetworkError() { + hideLoading() + error_dismiss.setText(R.string.ok) + error_message.setText(R.string.activity_iab_no_network_message) + generic_error_layout.visibility = View.VISIBLE + } + + override fun getOkErrorClick() = RxView.clicks(error_dismiss) + + override fun getSupportIconClick() = RxView.clicks(layout_support_icn) + + override fun getSupportLogoClick() = RxView.clicks(layout_support_logo) + + override fun close() = iabView.close(billingMessagesMapper.mapCancellation()) + + override fun showError(message: Int?) { + error_dismiss.setText(R.string.ok) + error_message.text = getString(message ?: R.string.activity_iab_error_message) + generic_error_layout.visibility = View.VISIBLE + hideLoading() + } + + override fun finish(uid: String?) { + presenter.sendPaymentEvent() + presenter.sendRevenueEvent() + presenter.sendPaymentSuccessEvent() + val bundle = billingMessagesMapper.successBundle(uid) + bundle.putString(InAppPurchaseInteractor.PRE_SELECTED_PAYMENT_METHOD_KEY, + PaymentMethodsView.PaymentMethodId.APPC_CREDITS.id) + iabView.finish(bundle) + } + + override fun errorClose() = iabView.close(billingMessagesMapper.genericError()) + + override fun finish(purchase: Purchase, orderReference: String?) { + presenter.sendPaymentEvent() + presenter.sendRevenueEvent() + presenter.sendPaymentSuccessEvent() + val bundle = billingMessagesMapper.mapPurchase(purchase, orderReference) + bundle.putString(InAppPurchaseInteractor.PRE_SELECTED_PAYMENT_METHOD_KEY, + PaymentMethodsView.PaymentMethodId.APPC_CREDITS.id) + iabView.finish(bundle) + } + + override fun showWalletValidation(@StringRes error: Int) = iabView.showWalletValidation(error) + + override fun showTransactionCompleted() { + loading_view.visibility = View.GONE + generic_error_layout.visibility = View.GONE + iab_activity_transaction_completed.visibility = View.VISIBLE + } + + override fun getAnimationDuration() = lottie_transaction_success.duration + + override fun lockRotation() = iabView.lockRotation() + + override fun onAttach(context: Context) { + super.onAttach(context) + check(context is IabView) { "AppcoinsRewardsBuyFragment must be attached to IAB activity" } + iabView = context + } + + private fun setupTransactionCompleteAnimation() = + lottie_transaction_success.setAnimation(R.raw.success_animation) + + private val amount: BigDecimal by lazy { + if (arguments!!.containsKey(AMOUNT_KEY)) { + arguments!!.getSerializable(AMOUNT_KEY) as BigDecimal + } else { + throw IllegalArgumentException("amount data not found") + } + } + + private val uri: String by lazy { + if (arguments!!.containsKey(URI_KEY)) { + arguments!!.getString(URI_KEY, "") + } else { + throw IllegalArgumentException("uri not found") + } + } + + private val isBds: Boolean by lazy { + if (arguments!!.containsKey(IS_BDS)) { + arguments!!.getBoolean(IS_BDS) + } else { + throw IllegalArgumentException("isBds not found") + } + } + + private val gamificationLevel: Int by lazy { + if (arguments!!.containsKey(GAMIFICATION_LEVEL)) { + arguments!!.getInt(GAMIFICATION_LEVEL) + } else { + throw IllegalArgumentException("gamification level data not found") + } + } + + private val transactionBuilder: TransactionBuilder by lazy { + if (arguments!!.containsKey(TRANSACTION_KEY)) { + arguments!!.getParcelable(TRANSACTION_KEY) as TransactionBuilder + } else { + throw IllegalArgumentException("transaction data not found") + } + } + + companion object { + private const val AMOUNT_KEY = "amount" + private const val URI_KEY = "uri_key" + private const val IS_BDS = "is_bds" + private const val TRANSACTION_KEY = "transaction_key" + private const val GAMIFICATION_LEVEL = "gamification_level" + + fun newInstance(amount: BigDecimal, transactionBuilder: TransactionBuilder, + uri: String?, isBds: Boolean, + gamificationLevel: Int): Fragment { + return AppcoinsRewardsBuyFragment().apply { + arguments = Bundle().apply { + putSerializable(AMOUNT_KEY, amount) + putParcelable(TRANSACTION_KEY, transactionBuilder) + putString(URI_KEY, uri) + putBoolean(IS_BDS, isBds) + putInt(GAMIFICATION_LEVEL, gamificationLevel) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsRewardsBuyInteract.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsRewardsBuyInteract.kt new file mode 100644 index 00000000000..60fd3053841 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsRewardsBuyInteract.kt @@ -0,0 +1,38 @@ +package com.asfoundation.wallet.ui.iab + +import com.appcoins.wallet.bdsbilling.WalletService +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import io.reactivex.Completable +import io.reactivex.Single +import java.util.* + +class AppcoinsRewardsBuyInteract(private val inAppPurchaseInteractor: InAppPurchaseInteractor, + private val supportInteractor: SupportInteractor, + private val walletService: WalletService, + private val walletBlockedInteract: WalletBlockedInteract, + private val smsValidationInteract: SmsValidationInteract) { + + fun isWalletBlocked() = walletBlockedInteract.isWalletBlocked() + + fun isWalletVerified() = + walletService.getWalletAddress() + .flatMap { smsValidationInteract.isValidated(it) } + .onErrorReturn { true } + + fun showSupport(gamificationLevel: Int): Completable { + return walletService.getWalletAddress() + .flatMapCompletable { + Completable.fromAction { + supportInteractor.registerUser(gamificationLevel, it.toLowerCase(Locale.ROOT)) + supportInteractor.displayChatScreen() + } + } + } + + fun removeAsyncLocalPayment() = inAppPurchaseInteractor.removeAsyncLocalPayment() + + fun convertToFiat(appcValue: Double, currency: String): Single = + inAppPurchaseInteractor.convertToFiat(appcValue, currency) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsRewardsBuyPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsRewardsBuyPresenter.kt new file mode 100644 index 00000000000..6e75e9c81e9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsRewardsBuyPresenter.kt @@ -0,0 +1,213 @@ +package com.asfoundation.wallet.ui.iab + +import com.appcoins.wallet.appcoins.rewards.Transaction +import com.appcoins.wallet.billing.repository.entity.TransactionData +import com.asf.wallet.R +import com.asfoundation.wallet.analytics.FacebookEventLogger +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.TransferParser +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import java.math.BigDecimal +import java.util.concurrent.TimeUnit + +class AppcoinsRewardsBuyPresenter(private val view: AppcoinsRewardsBuyView, + private val rewardsManager: RewardsManager, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val amount: BigDecimal, + private val uri: String, + private val packageName: String, + private val transferParser: TransferParser, + private val isBds: Boolean, + private val analytics: BillingAnalytics, + private val transactionBuilder: TransactionBuilder, + private val formatter: CurrencyFormatUtils, + private val gamificationLevel: Int, + private val appcoinsRewardsBuyInteract: AppcoinsRewardsBuyInteract, + private val logger: Logger) { + + companion object { + private val TAG = AppcoinsRewardsBuyPresenter::class.java.name + } + + fun present() { + view.lockRotation() + handleBuyClick() + handleOkErrorClick() + handleSupportClicks() + } + + private fun handleOkErrorClick() { + disposables.add(view.getOkErrorClick() + .doOnNext { view.errorClose() } + .subscribe({}, { + logger.log(TAG, "Ok error click", it) + view.errorClose() + })) + } + + private fun handleBuyClick() { + disposables.add(transferParser.parse(uri) + .flatMapCompletable { transaction: TransactionBuilder -> + rewardsManager.pay(transaction.skuId, amount, transaction.toAddress(), packageName, + getOrigin(isBds, transaction), transaction.type, transaction.payload, + transaction.callbackUrl, transaction.orderReference, transaction.referrerUrl) + .andThen(rewardsManager.getPaymentStatus(packageName, transaction.skuId, + transaction.amount())) + .observeOn(viewScheduler) + .flatMapCompletable { paymentStatus: RewardPayment -> + handlePaymentStatus(paymentStatus, transaction.skuId, transaction.amount()) + } + } + .doOnSubscribe { view.showLoading() } + .subscribe({}, { + logger.log(TAG, it) + view.showError(null) + })) + } + + private fun getOrigin(isBds: Boolean, transaction: TransactionBuilder): String? { + return if (transaction.origin == null) { + if (isBds) "BDS" else null + } else { + transaction.origin + } + } + + private fun handlePaymentStatus(transaction: RewardPayment, sku: String?, + amount: BigDecimal): Completable { + sendPaymentErrorEvent(transaction) + return when (transaction.status) { + Status.PROCESSING -> Completable.fromAction { view.showLoading() } + Status.COMPLETED -> { + if (isBds && transactionBuilder.type.equals(TransactionData.TransactionType.INAPP.name, + ignoreCase = true)) { + rewardsManager.getPaymentCompleted(packageName, sku) + .flatMapCompletable { purchase -> + Completable.fromAction { view.showTransactionCompleted() } + .subscribeOn(viewScheduler) + .andThen(Completable.timer(view.getAnimationDuration(), TimeUnit.MILLISECONDS)) + .andThen( + Completable.fromAction { appcoinsRewardsBuyInteract.removeAsyncLocalPayment() }) + .andThen(Completable.fromAction { + view.finish(purchase, transaction.orderReference) + }) + } + .observeOn(viewScheduler) + .onErrorResumeNext { + Completable.fromAction { + logger.log(TAG, "Error after completing the transaction", it) + view.showError(null) + view.hideLoading() + } + } + } else { + rewardsManager.getTransaction(packageName, sku, amount) + .firstOrError() + .map(Transaction::txId) + .flatMapCompletable { transactionId -> + Completable.fromAction { view.showTransactionCompleted() } + .subscribeOn(viewScheduler) + .andThen(Completable.timer(view.getAnimationDuration(), TimeUnit.MILLISECONDS)) + .andThen(Completable.fromAction { view.finish(transactionId) }) + } + } + } + Status.ERROR -> Completable.fromAction { + logger.log(TAG, "Credits error: ${transaction.errorMessage}") + view.showError(null) + } + Status.FORBIDDEN -> Completable.fromAction { + handleFraudFlow() + } + Status.NO_NETWORK -> Completable.fromAction { + view.showNoNetworkError() + view.hideLoading() + } + } + } + + private fun handleFraudFlow() { + disposables.add( + appcoinsRewardsBuyInteract.isWalletBlocked() + .subscribeOn(networkScheduler) + .observeOn(networkScheduler) + .flatMap { blocked -> + if (blocked) { + appcoinsRewardsBuyInteract.isWalletVerified() + .observeOn(viewScheduler) + .doOnSuccess { + if (it) view.showError(R.string.purchase_error_wallet_block_code_403) + else view.showWalletValidation(R.string.unknown_error) + } + } else { + Single.just(true) + .observeOn(viewScheduler) + .doOnSuccess { view.showError(R.string.purchase_error_wallet_block_code_403) } + } + } + .observeOn(viewScheduler) + .subscribe({}, { + logger.log(TAG, it) + view.showError(R.string.purchase_error_wallet_block_code_403) + }) + ) + } + + fun stop() = disposables.clear() + + fun sendPaymentEvent() { + analytics.sendPaymentEvent(packageName, transactionBuilder.skuId, + transactionBuilder.amount() + .toString(), BillingAnalytics.PAYMENT_METHOD_REWARDS, transactionBuilder.type) + } + + fun sendRevenueEvent() { + analytics.sendRevenueEvent(formatter.scaleFiat(appcoinsRewardsBuyInteract.convertToFiat( + transactionBuilder.amount() + .toDouble(), FacebookEventLogger.EVENT_REVENUE_CURRENCY) + .blockingGet() + .amount) + .toString()) + } + + fun sendPaymentSuccessEvent() { + analytics.sendPaymentSuccessEvent(packageName, transactionBuilder.skuId, + transactionBuilder.amount() + .toString(), BillingAnalytics.PAYMENT_METHOD_REWARDS, transactionBuilder.type) + } + + private fun sendPaymentErrorEvent(transaction: RewardPayment) { + val status = transaction.status + if (status === Status.ERROR || status === Status.NO_NETWORK || status === Status.FORBIDDEN) { + if (transaction.errorCode == null && transaction.errorMessage == null) { + analytics.sendPaymentErrorEvent(packageName, transactionBuilder.skuId, + transactionBuilder.amount() + .toString(), BillingAnalytics.PAYMENT_METHOD_REWARDS, transactionBuilder.type, + status.toString()) + } else { + analytics.sendPaymentErrorWithDetailsEvent(packageName, transactionBuilder.skuId, + transactionBuilder.amount() + .toString(), BillingAnalytics.PAYMENT_METHOD_REWARDS, transactionBuilder.type, + transaction.errorCode.toString(), transaction.errorMessage.toString()) + } + } + } + + private fun handleSupportClicks() { + disposables.add(Observable.merge(view.getSupportIconClick(), + view.getSupportLogoClick()) + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .flatMapCompletable { appcoinsRewardsBuyInteract.showSupport(gamificationLevel) } + .subscribe({}, { it.printStackTrace() })) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsRewardsBuyView.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsRewardsBuyView.kt new file mode 100644 index 00000000000..3a9e01bd63d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/AppcoinsRewardsBuyView.kt @@ -0,0 +1,40 @@ +package com.asfoundation.wallet.ui.iab + +import androidx.annotation.StringRes +import com.appcoins.wallet.bdsbilling.repository.entity.Purchase +import io.reactivex.Observable + +interface AppcoinsRewardsBuyView { + + fun finish(purchase: Purchase) + + fun showLoading() + + fun hideLoading() + + fun showNoNetworkError() + + fun getOkErrorClick(): Observable + + fun getSupportIconClick(): Observable + + fun getSupportLogoClick(): Observable + + fun close() + + fun showError(message: Int?) + + fun finish(uid: String?) + + fun errorClose() + + fun finish(purchase: Purchase, orderReference: String?) + + fun showTransactionCompleted() + + fun getAnimationDuration(): Long + + fun lockRotation() + + fun showWalletValidation(@StringRes error: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/ApproveKeyProvider.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/ApproveKeyProvider.java new file mode 100644 index 00000000000..cd3db44b8d6 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/ApproveKeyProvider.java @@ -0,0 +1,19 @@ +package com.asfoundation.wallet.ui.iab; + +import com.appcoins.wallet.bdsbilling.Billing; +import com.appcoins.wallet.bdsbilling.repository.entity.Transaction; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +public class ApproveKeyProvider { + private final Billing billing; + + public ApproveKeyProvider(Billing billing) { + this.billing = billing; + } + + Single getKey(String packageName, String productName) { + return billing.getSkuTransaction(packageName, productName, Schedulers.io()) + .map(Transaction::getUid); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/AsfInAppPurchaseInteractor.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/AsfInAppPurchaseInteractor.java new file mode 100644 index 00000000000..5053cfc1076 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/AsfInAppPurchaseInteractor.java @@ -0,0 +1,339 @@ +package com.asfoundation.wallet.ui.iab; + +import androidx.annotation.NonNull; +import com.appcoins.wallet.bdsbilling.Billing; +import com.appcoins.wallet.bdsbilling.repository.entity.Purchase; +import com.appcoins.wallet.bdsbilling.repository.entity.Transaction; +import com.appcoins.wallet.billing.BillingMessagesMapper; +import com.appcoins.wallet.billing.repository.entity.TransactionData; +import com.asfoundation.wallet.entity.GasSettings; +import com.asfoundation.wallet.entity.TransactionBuilder; +import com.asfoundation.wallet.interact.FetchGasSettingsInteract; +import com.asfoundation.wallet.interact.FindDefaultWalletInteract; +import com.asfoundation.wallet.interact.GetDefaultWalletBalanceInteract; +import com.asfoundation.wallet.repository.BdsTransactionService; +import com.asfoundation.wallet.repository.CurrencyConversionService; +import com.asfoundation.wallet.repository.InAppPurchaseService; +import com.asfoundation.wallet.repository.PaymentTransaction; +import com.asfoundation.wallet.repository.TransactionNotFoundException; +import com.asfoundation.wallet.util.TransferParser; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; +import java.math.BigDecimal; +import java.net.UnknownServiceException; +import java.util.ArrayList; +import java.util.List; + +public class AsfInAppPurchaseInteractor { + private static final double GAS_PRICE_MULTIPLIER = 1.25; + private final InAppPurchaseService inAppPurchaseService; + private final CurrencyConversionService currencyConversionService; + private final FindDefaultWalletInteract defaultWalletInteract; + private final FetchGasSettingsInteract gasSettingsInteract; + private final BigDecimal paymentGasLimit; + private final TransferParser parser; + private final BillingMessagesMapper billingMessagesMapper; + private final Billing billing; + private final BdsTransactionService trackTransactionService; + private final Scheduler scheduler; + + public AsfInAppPurchaseInteractor(InAppPurchaseService inAppPurchaseService, + FindDefaultWalletInteract defaultWalletInteract, FetchGasSettingsInteract gasSettingsInteract, + BigDecimal paymentGasLimit, TransferParser parser, + BillingMessagesMapper billingMessagesMapper, Billing billing, + CurrencyConversionService currencyConversionService, + BdsTransactionService trackTransactionService, Scheduler scheduler) { + this.inAppPurchaseService = inAppPurchaseService; + this.defaultWalletInteract = defaultWalletInteract; + this.gasSettingsInteract = gasSettingsInteract; + this.paymentGasLimit = paymentGasLimit; + this.parser = parser; + this.billingMessagesMapper = billingMessagesMapper; + this.billing = billing; + this.currencyConversionService = currencyConversionService; + this.trackTransactionService = trackTransactionService; + this.scheduler = scheduler; + } + + Single parseTransaction(String uri) { + return parser.parse(uri); + } + + public Completable send(String uri, TransactionType transactionType, String packageName, + String productName, String developerPayload) { + if (transactionType == TransactionType.NORMAL) { + return buildPaymentTransaction(uri, packageName, productName, + developerPayload).flatMapCompletable( + paymentTransaction -> inAppPurchaseService.send(paymentTransaction.getUri(), + paymentTransaction)); + } + return Completable.error(new UnsupportedOperationException( + "Transaction type " + transactionType + " not supported")); + } + + Completable resume(String uri, TransactionType transactionType, String packageName, + String productName, String approveKey, String developerPayload) { + if (transactionType == TransactionType.NORMAL) { + return buildPaymentTransaction(uri, packageName, productName, + developerPayload).flatMapCompletable( + paymentTransaction -> billing.getSkuTransaction(packageName, + paymentTransaction.getTransactionBuilder() + .getSkuId(), scheduler) + .flatMapCompletable( + transaction -> resumePayment(approveKey, paymentTransaction, transaction))); + } + return Completable.error(new UnsupportedOperationException( + "Transaction type " + transactionType + " not supported")); + } + + private Completable resumePayment(String approveKey, PaymentTransaction paymentTransaction, + Transaction transaction) { + switch (transaction.getStatus()) { + case PENDING_SERVICE_AUTHORIZATION: + return inAppPurchaseService.resume(paymentTransaction.getUri(), + new PaymentTransaction(paymentTransaction, PaymentTransaction.PaymentState.APPROVED, + approveKey)); + case PROCESSING: + return trackTransactionService.trackTransaction(paymentTransaction.getUri(), + paymentTransaction.getPackageName(), paymentTransaction.getTransactionBuilder() + .getSkuId(), transaction.getUid(), transaction.getOrderReference()); + case PENDING: + case COMPLETED: + case INVALID_TRANSACTION: + case FAILED: + case CANCELED: + default: + return Completable.error(new UnsupportedOperationException( + "Cannot resume from " + transaction.getStatus() + " state")); + } + } + + Observable getTransactionState(String uri) { + return Observable.merge(inAppPurchaseService.getTransactionState(uri) + .map(this::mapToPayment), trackTransactionService.getTransaction(uri) + .map(this::map)); + } + + private Payment map(BdsTransactionService.BdsTransaction transaction) { + return new Payment(transaction.getKey(), mapStatus(transaction.getStatus()), null, null, + transaction.getPackageName(), null, transaction.getSkuId(), transaction.getOrderReference(), + null, null); + } + + private Payment.Status mapStatus(BdsTransactionService.BdsTransaction.Status status) { + switch (status) { + default: + case WAITING: + case UNKNOWN_STATUS: + return Payment.Status.ERROR; + case PROCESSING: + return Payment.Status.BUYING; + case COMPLETED: + return Payment.Status.COMPLETED; + } + } + + @NonNull private Payment mapToPayment(PaymentTransaction paymentTransaction) { + return new Payment(paymentTransaction.getUri(), mapStatus(paymentTransaction.getState()), + paymentTransaction.getTransactionBuilder() + .fromAddress(), paymentTransaction.getBuyHash(), paymentTransaction.getPackageName(), + paymentTransaction.getProductName(), paymentTransaction.getProductId(), + paymentTransaction.getOrderReference(), paymentTransaction.getErrorCode(), + paymentTransaction.getErrorMessage()); + } + + private Payment.Status mapStatus(PaymentTransaction.PaymentState state) { + switch (state) { + case PENDING: + case APPROVING: + case APPROVED: + return Payment.Status.APPROVING; + case BUYING: + case BOUGHT: + return Payment.Status.BUYING; + case COMPLETED: + return Payment.Status.COMPLETED; + case ERROR: + return Payment.Status.ERROR; + case WRONG_NETWORK: + case UNKNOWN_TOKEN: + return Payment.Status.NETWORK_ERROR; + case NONCE_ERROR: + return Payment.Status.NONCE_ERROR; + case NO_TOKENS: + return Payment.Status.NO_TOKENS; + case NO_ETHER: + return Payment.Status.NO_ETHER; + case NO_FUNDS: + return Payment.Status.NO_FUNDS; + case NO_INTERNET: + return Payment.Status.NO_INTERNET; + case FORBIDDEN: + return Payment.Status.FORBIDDEN; + } + throw new IllegalStateException("State " + state + " not mapped"); + } + + public Completable remove(String uri) { + return inAppPurchaseService.remove(uri) + .andThen(trackTransactionService.remove(uri)); + } + + private Single buildPaymentTransaction(String uri, String packageName, + String productName, String developerPayload) { + return Single.zip(parseTransaction(uri).observeOn(scheduler), defaultWalletInteract.find() + .observeOn(scheduler), (transaction, wallet) -> transaction.fromAddress(wallet.address)) + .flatMap(transactionBuilder -> gasSettingsInteract.fetch(true) + .map(gasSettings -> transactionBuilder.gasSettings( + new GasSettings(gasSettings.gasPrice.multiply(new BigDecimal(GAS_PRICE_MULTIPLIER)), + paymentGasLimit)))) + .map(transactionBuilder -> new PaymentTransaction(uri, transactionBuilder, packageName, + productName, transactionBuilder.getSkuId(), developerPayload, + transactionBuilder.getCallbackUrl(), transactionBuilder.getOrderReference())); + } + + public void start() { + inAppPurchaseService.start(); + trackTransactionService.start(); + } + + public Observable> getAll() { + return inAppPurchaseService.getAll() + .flatMapSingle(paymentTransactions -> Observable.fromIterable(paymentTransactions) + .map(paymentTransaction -> new Payment(paymentTransaction.getUri(), + mapStatus(paymentTransaction.getState()), paymentTransaction.getTransactionBuilder() + .fromAddress(), paymentTransaction.getBuyHash(), + paymentTransaction.getPackageName(), paymentTransaction.getProductName(), + paymentTransaction.getProductId(), null, paymentTransaction.getErrorCode(), + paymentTransaction.getErrorMessage())) + .toList()); + } + + List getTopUpChannelSuggestionValues(BigDecimal price) { + BigDecimal firstValue = + price.add(new BigDecimal(5).subtract((price.remainder(new BigDecimal(5))))); + ArrayList list = new ArrayList<>(); + list.add(price); + list.add(firstValue); + list.add(firstValue.add(new BigDecimal(5))); + list.add(firstValue.add(new BigDecimal(15))); + list.add(firstValue.add(new BigDecimal(25))); + return list; + } + + public Single getWalletAddress() { + return defaultWalletInteract.find() + .map(wallet -> wallet.address); + } + + Single getCurrentPaymentStep(String packageName, + TransactionBuilder transactionBuilder) { + return Single.zip( + getTransaction(packageName, transactionBuilder.getSkuId(), transactionBuilder.getType()), + isAppcoinsPaymentReady(transactionBuilder), this::map); + } + + Single isAppcoinsPaymentReady(TransactionBuilder transactionBuilder) { + return gasSettingsInteract.fetch(true) + .doOnSuccess(gasSettings -> transactionBuilder.gasSettings( + new GasSettings(gasSettings.gasPrice.multiply(new BigDecimal(GAS_PRICE_MULTIPLIER)), + paymentGasLimit))) + .flatMap(__ -> inAppPurchaseService.hasBalanceToBuy(transactionBuilder)); + } + + Single getAppcoinsBalanceState( + TransactionBuilder transactionBuilder) { + return gasSettingsInteract.fetch(true) + .doOnSuccess(gasSettings -> transactionBuilder.gasSettings( + new GasSettings(gasSettings.gasPrice.multiply(new BigDecimal(GAS_PRICE_MULTIPLIER)), + paymentGasLimit))) + .flatMap(__ -> inAppPurchaseService.getBalanceState(transactionBuilder)); + } + + private CurrentPaymentStep map(Transaction transaction, Boolean isBuyReady) + throws UnknownServiceException { + switch (transaction.getStatus()) { + case PENDING: + case PENDING_SERVICE_AUTHORIZATION: + case PROCESSING: + switch (transaction.getGateway() + .getName()) { + case appcoins: + return CurrentPaymentStep.PAUSED_ON_CHAIN; + case adyen_v2: + if (transaction.getStatus() + .equals(Transaction.Status.PROCESSING)) { + return CurrentPaymentStep.PAUSED_CC_PAYMENT; + } else { + return isBuyReady ? CurrentPaymentStep.READY : CurrentPaymentStep.NO_FUNDS; + } + case myappcoins: + return CurrentPaymentStep.PAUSED_LOCAL_PAYMENT; + case appcoins_credits: + return CurrentPaymentStep.PAUSED_CREDITS; + default: + case unknown: + throw new UnknownServiceException("Unknown gateway"); + } + case COMPLETED: + case PENDING_USER_PAYMENT: + case FAILED: + case CANCELED: + case INVALID_TRANSACTION: + default: + return isBuyReady ? CurrentPaymentStep.READY : CurrentPaymentStep.NO_FUNDS; + } + } + + Single convertToFiat(double appcValue, String currency) { + return currencyConversionService.getTokenValue(currency) + .map(fiatValueConversion -> calculateValue(fiatValueConversion, appcValue)); + } + + Single convertToLocalFiat(double appcValue) { + return currencyConversionService.getLocalFiatAmount(Double.toString(appcValue)); + } + + private FiatValue calculateValue(FiatValue fiatValue, double appcValue) { + return new FiatValue(fiatValue.getAmount() + .multiply(BigDecimal.valueOf(appcValue)), fiatValue.getCurrency(), fiatValue.getSymbol()); + } + + public BillingMessagesMapper getBillingMessagesMapper() { + return billingMessagesMapper; + } + + private Single getTransaction(String packageName, String productName, String type) { + return Single.defer(() -> { + if (TransactionData.TransactionType.INAPP.name() + .equalsIgnoreCase(type)) { + return billing.getSkuTransaction(packageName, productName, Schedulers.io()); + } else { + return Single.just(Transaction.Companion.notFound()); + } + }); + } + + Single getCompletedPurchase(String packageName, String productName) { + return billing.getSkuTransaction(packageName, productName, Schedulers.io()) + .map(Transaction::getStatus) + .flatMap(transactionStatus -> { + if (transactionStatus.equals(Transaction.Status.COMPLETED)) { + return billing.getSkuPurchase(packageName, productName, Schedulers.io()); + } else { + return Single.error(new TransactionNotFoundException()); + } + }); + } + + public enum TransactionType { + NORMAL + } + + public enum CurrentPaymentStep { + PAUSED_CC_PAYMENT, PAUSED_ON_CHAIN, NO_FUNDS, PAUSED_LOCAL_PAYMENT, PAUSED_CREDITS, READY + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/Availability.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/Availability.kt new file mode 100644 index 00000000000..e68de8bfb10 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/Availability.kt @@ -0,0 +1,3 @@ +package com.asfoundation.wallet.ui.iab + +data class Availability(val isAvailable: Boolean, val disableReason: Int?) diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/BdsInAppPurchaseInteractor.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/BdsInAppPurchaseInteractor.java new file mode 100644 index 00000000000..f53bd581bcb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/BdsInAppPurchaseInteractor.java @@ -0,0 +1,88 @@ +package com.asfoundation.wallet.ui.iab; + +import com.appcoins.wallet.bdsbilling.Billing; +import com.appcoins.wallet.bdsbilling.BillingPaymentProofSubmission; +import com.appcoins.wallet.bdsbilling.repository.entity.PaymentMethodEntity; +import com.appcoins.wallet.bdsbilling.repository.entity.Purchase; +import com.appcoins.wallet.billing.BillingMessagesMapper; +import com.asfoundation.wallet.entity.TransactionBuilder; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.Single; +import java.math.BigDecimal; +import java.util.List; + +public class BdsInAppPurchaseInteractor { + private final AsfInAppPurchaseInteractor inAppPurchaseInteractor; + private final BillingPaymentProofSubmission billingPaymentProofSubmission; + private final ApproveKeyProvider approveKeyProvider; + private final Billing billing; + + public BdsInAppPurchaseInteractor(AsfInAppPurchaseInteractor inAppPurchaseInteractor, + BillingPaymentProofSubmission billingPaymentProofSubmission, + ApproveKeyProvider approveKeyProvider, Billing billing) { + this.inAppPurchaseInteractor = inAppPurchaseInteractor; + this.billingPaymentProofSubmission = billingPaymentProofSubmission; + this.approveKeyProvider = approveKeyProvider; + this.billing = billing; + } + + public Single parseTransaction(String uri) { + return inAppPurchaseInteractor.parseTransaction(uri); + } + + public Completable send(String uri, AsfInAppPurchaseInteractor.TransactionType transactionType, + String packageName, String productName, String developerPayload) { + return inAppPurchaseInteractor.send(uri, transactionType, packageName, productName, + developerPayload); + } + + public Completable resume(String uri, AsfInAppPurchaseInteractor.TransactionType transactionType, + String packageName, String productName, String developerPayload) { + return approveKeyProvider.getKey(packageName, productName) + .doOnSuccess(billingPaymentProofSubmission::saveTransactionId) + .flatMapCompletable( + approveKey -> inAppPurchaseInteractor.resume(uri, transactionType, packageName, + productName, approveKey, developerPayload)); + } + + public Observable getTransactionState(String uri) { + return inAppPurchaseInteractor.getTransactionState(uri); + } + + public Completable remove(String uri) { + return inAppPurchaseInteractor.remove(uri); + } + + public void start() { + inAppPurchaseInteractor.start(); + } + + public Observable> getAll() { + return inAppPurchaseInteractor.getAll(); + } + + public List getTopUpChannelSuggestionValues(BigDecimal price) { + return inAppPurchaseInteractor.getTopUpChannelSuggestionValues(price); + } + + public Single getWalletAddress() { + return inAppPurchaseInteractor.getWalletAddress(); + } + + public BillingMessagesMapper getBillingMessagesMapper() { + return inAppPurchaseInteractor.getBillingMessagesMapper(); + } + + public Single getCompletedPurchase(String packageName, String productName) { + return inAppPurchaseInteractor.getCompletedPurchase(packageName, productName); + } + + public Single getWallet(String packageName) { + return billing.getWallet(packageName); + } + + public Single> getPaymentMethods(String value, String currency) { + return billing.getPaymentMethods(value, currency); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/BillingWebViewFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/BillingWebViewFragment.kt new file mode 100644 index 00000000000..fbdb742b2a9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/BillingWebViewFragment.kt @@ -0,0 +1,182 @@ +package com.asfoundation.wallet.ui.iab + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.CookieManager +import android.webkit.WebView +import android.webkit.WebViewClient +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.google.android.material.snackbar.Snackbar +import kotlinx.android.synthetic.main.webview_fragment.* +import kotlinx.android.synthetic.main.webview_fragment.view.* +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject + +class BillingWebViewFragment : BasePageViewFragment() { + private val timeoutReference: AtomicReference?> = AtomicReference() + + @Inject + lateinit var inAppPurchaseInteractor: InAppPurchaseInteractor + + @Inject + lateinit var analytics: BillingAnalytics + private var currentUrl: String? = null + private var executorService: ScheduledExecutorService? = null + private var webViewActivity: WebViewActivity? = null + private var asyncDetailsShown = false + + companion object { + private const val ADYEN_PAYMENT_SCHEMA = "adyencheckout://" + private const val LOCAL_PAYMENTS_SCHEMA = "myappcoins.com/t/" + private const val LOCAL_PAYMENTS_URL = "https://myappcoins.com/t/" + private const val GO_PAY_APP_PAYMENTS_SCHEMA = "gojek://" + private const val LINE_APP_PAYMENTS_SCHEMA = "intent://" + private const val ASYNC_PAYMENT_FORM_SHOWN_SCHEMA = "https://pm.dlocal.com//v1/gateway/show?" + private const val CODAPAY_FINAL_REDIRECT_SCHEMA = + "https://airtime.codapayments.com/epcgw/dlocal/" + private const val CODAPAY_BACK_URL = + "https://pay.dlocal.com/payment_method_connectors/global_pm//back" + private const val CODAPAY_CANCEL_URL = + "codapayments.com/airtime/cancelConfirm" + private const val URL = "url" + private const val CURRENT_URL = "currentUrl" + private const val ORDER_ID_PARAMETER = "OrderId" + + fun newInstance(url: String?): BillingWebViewFragment { + return BillingWebViewFragment().apply { + arguments = Bundle().apply { + putString(URL, url) + } + retainInstance = true + } + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + require(context is WebViewActivity) { "WebView fragment must be attached to WebView Activity" } + webViewActivity = context + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + executorService = Executors.newScheduledThreadPool(0) + require(arguments != null && arguments!!.containsKey(URL)) { "Provided url is null!" } + currentUrl = if (savedInstanceState == null) { + arguments!!.getString(URL) + } else { + savedInstanceState.getString(CURRENT_URL) + } + CookieManager.getInstance() + .setAcceptCookie(true) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.webview_fragment, container, false) + + view.webview.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView, clickUrl: String): Boolean { + if (clickUrl.contains(LOCAL_PAYMENTS_SCHEMA) || clickUrl.contains(ADYEN_PAYMENT_SCHEMA)) { + currentUrl = clickUrl + finishWithSuccess(clickUrl) + } else if (clickUrl.contains(GO_PAY_APP_PAYMENTS_SCHEMA) || clickUrl.contains( + LINE_APP_PAYMENTS_SCHEMA)) { + launchActivity(Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl))) + } else if (clickUrl.contains(CODAPAY_FINAL_REDIRECT_SCHEMA) && clickUrl.contains( + ORDER_ID_PARAMETER)) { + val orderId = Uri.parse(clickUrl) + .getQueryParameter(ORDER_ID_PARAMETER) + finishWithSuccess(LOCAL_PAYMENTS_URL + orderId) + } else if (clickUrl.contains(CODAPAY_CANCEL_URL)) { + finishWithFail(clickUrl) + } else { + currentUrl = clickUrl + return false + } + return true + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + if (!url.contains("/redirect")) { + val timeout = timeoutReference.getAndSet(null) + timeout?.cancel(false) + webview_progress_bar?.visibility = View.GONE + } + if (url.contains(ASYNC_PAYMENT_FORM_SHOWN_SCHEMA)) { + asyncDetailsShown = true + } + } + } + view.webview.settings.javaScriptEnabled = true + view.webview.settings.domStorageEnabled = true + view.webview.settings.useWideViewPort = true + view.webview.loadUrl(currentUrl) + return view + } + + fun handleBackPressed(): Boolean { + return if (asyncDetailsShown) { + webview.loadUrl(CODAPAY_BACK_URL) + asyncDetailsShown = false + true + } else { + false + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(CURRENT_URL, currentUrl) + } + + override fun onDestroy() { + executorService!!.shutdown() + super.onDestroy() + } + + override fun onDetach() { + webViewActivity = null + webview?.webViewClient = null + super.onDetach() + } + + private fun launchActivity(intent: Intent) { + try { + startActivity(intent) + } catch (exception: ActivityNotFoundException) { + exception.printStackTrace() + if (view != null) { + Snackbar.make(view!!, R.string.unknown_error, + Snackbar.LENGTH_SHORT) + .show() + } + } + } + + private fun finishWithSuccess(url: String) { + val intent = Intent() + intent.data = Uri.parse(url) + webViewActivity!!.setResult(WebViewActivity.SUCCESS, intent) + webViewActivity!!.finish() + } + + private fun finishWithFail(url: String) { + val intent = Intent() + intent.data = Uri.parse(url) + webViewActivity!!.setResult(WebViewActivity.FAIL, intent) + webViewActivity!!.finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/EarnAppcoinsFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/EarnAppcoinsFragment.kt new file mode 100644 index 00000000000..8ac4b4fd81e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/EarnAppcoinsFragment.kt @@ -0,0 +1,149 @@ +package com.asfoundation.wallet.ui.iab + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.dialog_buy_buttons_payment_methods.* +import kotlinx.android.synthetic.main.dialog_buy_buttons_payment_methods.view.* +import kotlinx.android.synthetic.main.earn_appcoins_layout.* +import java.math.BigDecimal +import javax.inject.Inject + +class EarnAppcoinsFragment : BasePageViewFragment(), EarnAppcoinsView { + + private lateinit var presenter: EarnAppcoinsPresenter + private lateinit var iabView: IabView + + @Inject + lateinit var analytics: BillingAnalytics + + override fun onCreate(savedInstanceState: Bundle?) { + if (savedInstanceState == null) { + analytics.sendPaymentEvent(domain, skuId, amount.toString(), + PAYMENT_METHOD_NAME, type) + } + presenter = EarnAppcoinsPresenter(this, CompositeDisposable(), AndroidSchedulers.mainThread()) + super.onCreate(savedInstanceState) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check(context is IabView) { "Earn Appcoins fragment must be attached to IAB activity" } + iabView = context + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + dialog_buy_buttons_payment_methods.buy_button.text = getString(R.string.discover_button) + dialog_buy_buttons_payment_methods.cancel_button.text = getString(R.string.back_button) + iabView.disableBack() + presenter.present() + super.onViewCreated(view, savedInstanceState) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.earn_appcoins_layout, container, false) + } + + override fun backButtonClick(): Observable { + return RxView.clicks(cancel_button) + } + + override fun discoverButtonClick(): Observable { + return RxView.clicks(buy_button) + } + + override fun navigateBack() { + iabView.showPaymentMethodsView() + } + + override fun backPressed() = iabView.backButtonPress() + + override fun navigateToAptoide() { + val intent = Intent(Intent.ACTION_VIEW).apply { + val packageManager = context?.packageManager + this.data = Uri.parse(APTOIDE_EARN_APPCOINS_DEEP_LINK) + val appsList = + packageManager?.queryIntentActivities(this, PackageManager.MATCH_DEFAULT_ONLY) + appsList?.first { it.activityInfo.packageName == "cm.aptoide.pt" } + ?.let { + setPackage(it.activityInfo.packageName) + } + } + iabView.launchIntent(intent) + } + + override fun onDestroyView() { + iabView.enableBack() + presenter.destroy() + super.onDestroyView() + } + + val domain: String by lazy { + if (arguments!!.containsKey(PARAM_DOMAIN)) { + arguments!!.getString(PARAM_DOMAIN, "") + } else { + throw IllegalArgumentException("Domain not found") + } + } + + val skuId: String? by lazy { + if (arguments!!.containsKey(PARAM_SKUID)) { + val value = arguments!!.getString(PARAM_SKUID) ?: return@lazy null + value + } else { + throw IllegalArgumentException("SkuId not found") + } + } + + val amount: BigDecimal by lazy { + if (arguments!!.containsKey(PARAM_AMOUNT)) { + val value = arguments!!.getSerializable(PARAM_AMOUNT) as BigDecimal + value + } else { + throw IllegalArgumentException("amount not found") + } + } + + val type: String by lazy { + if (arguments!!.containsKey(PARAM_TRANSACTION_TYPE)) { + arguments!!.getString(PARAM_TRANSACTION_TYPE, "") + } else { + throw IllegalArgumentException("type not found") + } + } + + companion object { + + @JvmStatic + fun newInstance(domain: String, skuId: String?, amount: BigDecimal, + type: String): EarnAppcoinsFragment = EarnAppcoinsFragment().apply { + arguments = Bundle().apply { + putString(PARAM_DOMAIN, domain) + putString(PARAM_SKUID, skuId) + putString(PARAM_TRANSACTION_TYPE, type) + putSerializable(PARAM_AMOUNT, amount) + } + } + + private const val APTOIDE_EARN_APPCOINS_DEEP_LINK = + "aptoide://cm.aptoide.pt/deeplink?name=appcoins_ads" + private const val PARAM_DOMAIN = "AMOUNT_DOMAIN" + private const val PARAM_SKUID = "AMOUNT_SKUID" + private const val PARAM_AMOUNT = "PARAM_AMOUNT" + private const val PARAM_TRANSACTION_TYPE = "PARAM_TRANSACTION_TYPE" + private const val PAYMENT_METHOD_NAME = "EARN_APPCOINS_BUNDLE" + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/EarnAppcoinsPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/EarnAppcoinsPresenter.kt new file mode 100644 index 00000000000..e394776b28c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/EarnAppcoinsPresenter.kt @@ -0,0 +1,32 @@ +package com.asfoundation.wallet.ui.iab + +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +class EarnAppcoinsPresenter(private val view: EarnAppcoinsView, + private val disposables: CompositeDisposable, + private val viewScheduler: Scheduler) { + fun present() { + handleBackClick() + handleDiscoverClick() + } + + private fun handleDiscoverClick() { + disposables.add(view.discoverButtonClick() + .observeOn(viewScheduler) + .doOnNext { view.navigateToAptoide() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleBackClick() { + disposables.add(Observable.merge(view.backButtonClick(), view.backPressed()) + .observeOn(viewScheduler) + .doOnNext { view.navigateBack() } + .subscribe({}, { it.printStackTrace() })) + } + + fun destroy() { + disposables.clear() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/EarnAppcoinsView.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/EarnAppcoinsView.kt new file mode 100644 index 00000000000..84bbd2a02aa --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/EarnAppcoinsView.kt @@ -0,0 +1,12 @@ +package com.asfoundation.wallet.ui.iab + +import io.reactivex.Observable + +interface EarnAppcoinsView { + + fun backButtonClick(): Observable + fun discoverButtonClick(): Observable + fun navigateBack() + fun navigateToAptoide() + fun backPressed(): Observable +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/FiatValue.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/FiatValue.kt new file mode 100644 index 00000000000..222db63fd86 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/FiatValue.kt @@ -0,0 +1,22 @@ +package com.asfoundation.wallet.ui.iab + +import java.io.Serializable +import java.math.BigDecimal + +data class FiatValue(val amount: BigDecimal, val currency: String, val symbol: String = "") : + Serializable { + + constructor() : this(BigDecimal.ZERO, "", "") + + override fun equals(other: Any?) = other is FiatValue + && other.amount.compareTo(this.amount) == 0 + && other.currency == this.currency + && other.symbol == this.symbol + + override fun hashCode(): Int { + var result = amount.hashCode() + result = 31 * result + currency.hashCode() + result = 31 * result + symbol.hashCode() + return result + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/FragmentNavigator.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/FragmentNavigator.java new file mode 100644 index 00000000000..98d8b4acc3c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/FragmentNavigator.java @@ -0,0 +1,33 @@ +package com.asfoundation.wallet.ui.iab; + +import android.net.Uri; +import android.os.Bundle; +import com.asfoundation.wallet.navigator.UriNavigator; +import io.reactivex.Observable; + +public class FragmentNavigator implements Navigator { + + private final UriNavigator uriNavigator; + private final IabView iabView; + + public FragmentNavigator(UriNavigator uriNavigator, IabView iabView) { + this.uriNavigator = uriNavigator; + this.iabView = iabView; + } + + @Override public void popView(Bundle bundle) { + iabView.finish(bundle); + } + + @Override public void popViewWithError() { + iabView.close(new Bundle()); + } + + @Override public void navigateToUriForResult(String redirectUrl) { + uriNavigator.navigateToUri(redirectUrl); + } + + @Override public Observable uriResults() { + return uriNavigator.uriResults(); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabActivity.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabActivity.java deleted file mode 100644 index 9d70dc8039c..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabActivity.java +++ /dev/null @@ -1,240 +0,0 @@ -package com.asfoundation.wallet.ui.iab; - -import android.app.Activity; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.annotation.StringRes; -import android.util.Pair; -import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.TransactionBuilder; -import com.asfoundation.wallet.ui.BaseActivity; -import com.jakewharton.rxbinding2.view.RxView; -import dagger.android.AndroidInjection; -import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.util.Formatter; -import java.util.Locale; -import javax.inject.Inject; - -/** - * Created by trinkes on 13/03/2018. - */ - -public class IabActivity extends BaseActivity implements IabView { - - public static final String APP_PACKAGE = "app_package"; - public static final String PRODUCT_NAME = "product_name"; - public static final String TRANSACTION_HASH = "transaction_hash"; - @Inject InAppPurchaseInteractor inAppPurchaseInteractor; - private Button buyButton; - private Button okErrorButton; - private IabPresenter presenter; - private View loadingView; - private TextView appName; - private TextView itemDescription; - private TextView itemPrice; - private ImageView appIcon; - private View transactionCompletedLayout; - private View transactionErrorLayout; - private View buyLayout; - private boolean isBackEnable; - private TextView errorTextView; - private TextView loadingMessage; - - public static Intent newIntent(Activity activity, Intent previousIntent) { - Intent intent = new Intent(activity, IabActivity.class); - intent.setData(previousIntent.getData()); - if (previousIntent.getExtras() != null) { - intent.putExtras(previousIntent.getExtras()); - } - intent.putExtra(APP_PACKAGE, activity.getCallingPackage()); - return intent; - } - - @Override public void onBackPressed() { - if (isBackEnable) { - super.onBackPressed(); - } - } - - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { - AndroidInjection.inject(this); - super.onCreate(savedInstanceState); - setContentView(R.layout.iab_activity); - buyButton = findViewById(R.id.buy_button); - okErrorButton = findViewById(R.id.activity_iab_error_ok_button); - loadingView = findViewById(R.id.loading); - loadingMessage = findViewById(R.id.loading_message); - appName = findViewById(R.id.iab_activity_app_name); - errorTextView = findViewById(R.id.activity_iab_error_message); - transactionCompletedLayout = findViewById(R.id.iab_activity_transaction_completed); - buyLayout = findViewById(R.id.iab_activity_buy_layout); - transactionErrorLayout = findViewById(R.id.activity_iab_error_view); - appIcon = findViewById(R.id.iab_activity_item_icon); - itemDescription = findViewById(R.id.iab_activity_item_description); - itemPrice = findViewById(R.id.iab_activity_item_price); - presenter = new IabPresenter(this, inAppPurchaseInteractor, AndroidSchedulers.mainThread(), - new CompositeDisposable()); - Single.defer(() -> Single.just(getAppPackage())) - .observeOn(Schedulers.io()) - .map(packageName -> new Pair<>(getApplicationName(packageName), - getPackageManager().getApplicationIcon(packageName))) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(pair -> { - appName.setText(pair.first); - appIcon.setImageDrawable(pair.second); - }, throwable -> { - throwable.printStackTrace(); - showError(); - }); - isBackEnable = true; - } - - @Override protected void onStart() { - super.onStart(); - presenter.present(getIntent().getData() - .toString(), getAppPackage(), getIntent().getExtras() - .getString(PRODUCT_NAME)); - } - - @Override protected void onStop() { - presenter.stop(); - super.onStop(); - } - - @Override public Observable getBuyClick() { - return RxView.clicks(buyButton) - .map(click -> getIntent().getData() - .toString()); - } - - @Override public Observable getCancelClick() { - return RxView.clicks(findViewById(R.id.cancel_button)); - } - - @Override public Observable getOkErrorClick() { - return RxView.clicks(okErrorButton); - } - - @Override public void finish(String hash) { - Intent intent = new Intent(); - intent.putExtra(TRANSACTION_HASH, hash); - setResult(Activity.RESULT_OK, intent); - finish(); - } - - @Override public void showLoading() { - showLoading(R.string.activity_aib_loading_message); - } - - @Override public void showError() { - showError(R.string.activity_iab_error_message); - } - - @Override public void setup(TransactionBuilder transactionBuilder) { - Formatter formatter = new Formatter(); - itemPrice.setText(formatter.format(Locale.getDefault(), "%(,.2f", transactionBuilder.amount() - .doubleValue()) - .toString()); - if (getIntent().hasExtra(PRODUCT_NAME)) { - itemDescription.setText(getIntent().getExtras() - .getString(PRODUCT_NAME)); - } - } - - @Override public void close() { - setResult(Activity.RESULT_CANCELED, null); - finish(); - } - - @Override public void showTransactionCompleted() { - loadingView.setVisibility(View.GONE); - transactionErrorLayout.setVisibility(View.GONE); - transactionCompletedLayout.setVisibility(View.VISIBLE); - buyLayout.setVisibility(View.GONE); - } - - @Override public void showBuy() { - loadingView.setVisibility(View.GONE); - transactionErrorLayout.setVisibility(View.GONE); - transactionCompletedLayout.setVisibility(View.GONE); - buyLayout.setVisibility(View.VISIBLE); - isBackEnable = true; - } - - @Override public void showWrongNetworkError() { - showError(R.string.activity_iab_wrong_network_message); - } - - @Override public void showNoNetworkError() { - showError(R.string.activity_iab_no_network_message); - } - - @Override public void showApproving() { - showLoading(R.string.activity_iab_approving_message); - } - - @Override public void showBuying() { - showLoading(R.string.activity_aib_buying_message); - } - - @Override public void showNonceError() { - showError(R.string.activity_iab_nonce_message); - } - - @Override public void showNoTokenFundsError() { - showError(R.string.activity_iab_no_token_funds_message); - } - - @Override public void showNoEtherFundsError() { - showError(R.string.activity_iab_no_ethereum_funds_message); - } - - @Override public void showNoFundsError() { - showError(R.string.activity_iab_no_funds_message); - } - - private void showLoading(@StringRes int message) { - isBackEnable = false; - loadingView.setVisibility(View.VISIBLE); - transactionErrorLayout.setVisibility(View.GONE); - transactionCompletedLayout.setVisibility(View.GONE); - buyLayout.setVisibility(View.GONE); - loadingMessage.setText(message); - loadingView.requestFocus(); - loadingView.setOnTouchListener((v, event) -> true); - } - - public void showError(int error_message) { - loadingView.setVisibility(View.GONE); - transactionErrorLayout.setVisibility(View.VISIBLE); - transactionCompletedLayout.setVisibility(View.GONE); - buyLayout.setVisibility(View.GONE); - isBackEnable = true; - errorTextView.setText(error_message); - } - - private CharSequence getApplicationName(String appPackage) - throws PackageManager.NameNotFoundException { - PackageManager packageManager = getPackageManager(); - ApplicationInfo packageInfo = packageManager.getApplicationInfo(appPackage, 0); - return packageManager.getApplicationLabel(packageInfo); - } - - public String getAppPackage() { - if (getIntent().hasExtra(APP_PACKAGE)) { - return getIntent().getStringExtra(APP_PACKAGE); - } - throw new IllegalArgumentException("previous app package name not found"); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabActivity.kt new file mode 100644 index 00000000000..2d474ae5fc9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabActivity.kt @@ -0,0 +1,413 @@ +package com.asfoundation.wallet.ui.iab + +import android.app.Activity +import android.content.Intent +import android.content.pm.ActivityInfo +import android.net.Uri +import android.os.Bundle +import android.view.View +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import com.appcoins.wallet.billing.AppcoinsBillingBinder +import com.appcoins.wallet.billing.AppcoinsBillingBinder.Companion.EXTRA_BDS_IAP +import com.appcoins.wallet.billing.repository.entity.TransactionData +import com.asf.wallet.R +import com.asfoundation.wallet.backup.BackupNotificationUtils +import com.asfoundation.wallet.billing.address.BillingAddressFragment +import com.asfoundation.wallet.billing.adyen.AdyenPaymentFragment +import com.asfoundation.wallet.billing.adyen.PaymentType +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.navigator.UriNavigator +import com.asfoundation.wallet.topup.TopUpActivity +import com.asfoundation.wallet.transactions.PerkBonusService +import com.asfoundation.wallet.ui.AuthenticationPromptActivity +import com.asfoundation.wallet.ui.BaseActivity +import com.asfoundation.wallet.ui.iab.IabInteract.Companion.PRE_SELECTED_PAYMENT_METHOD_KEY +import com.asfoundation.wallet.ui.iab.share.SharePaymentLinkFragment +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import com.asfoundation.wallet.wallet_validation.dialog.WalletValidationDialogActivity +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxrelay2.PublishRelay +import dagger.android.AndroidInjection +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.activity_iab.* +import kotlinx.android.synthetic.main.iab_error_layout.* +import kotlinx.android.synthetic.main.support_error_layout.* +import java.math.BigDecimal +import java.util.* +import javax.inject.Inject + +class IabActivity : BaseActivity(), IabView, UriNavigator { + + @Inject + lateinit var billingAnalytics: BillingAnalytics + + @Inject + lateinit var iabInteract: IabInteract + + @Inject + lateinit var walletBlockedInteract: WalletBlockedInteract + + @Inject + lateinit var logger: Logger + + private lateinit var presenter: IabPresenter + private var isBackEnable: Boolean = false + private var transaction: TransactionBuilder? = null + private var isBds: Boolean = false + private var backButtonPress: PublishRelay? = null + private var results: PublishRelay? = null + private var developerPayload: String? = null + private var uri: String? = null + private var authenticationResultSubject: PublishSubject? = null + + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + backButtonPress = PublishRelay.create() + results = PublishRelay.create() + authenticationResultSubject = PublishSubject.create() + setContentView(R.layout.activity_iab) + isBds = intent.getBooleanExtra(IS_BDS_EXTRA, false) + developerPayload = intent.getStringExtra(DEVELOPER_PAYLOAD) + uri = intent.getStringExtra(URI) + transaction = intent.getParcelableExtra(TRANSACTION_EXTRA) + isBackEnable = true + presenter = IabPresenter(this, Schedulers.io(), AndroidSchedulers.mainThread(), + CompositeDisposable(), billingAnalytics, iabInteract, logger, transaction) + presenter.present(savedInstanceState) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + presenter.onActivityResult(requestCode, resultCode, data) + } + + override fun onResume() { + super.onResume() + //The present is set here due to the Can not perform this action after onSaveInstanceState + //This assures that doesn't + presenter.onResume() + } + + override fun onBackPressed() { + if (isBackEnable) { + Bundle().apply { + putInt(RESPONSE_CODE, RESULT_USER_CANCELED) + close(this) + } + super.onBackPressed() + } else { + backButtonPress?.accept(Unit) + } + } + + override fun disableBack() { + isBackEnable = false + } + + override fun enableBack() { + isBackEnable = true + } + + override fun navigateBack() { + if (supportFragmentManager.backStackEntryCount != 0) { + supportFragmentManager.popBackStack() + } + } + + override fun finishActivity(data: Bundle) { + presenter.savePreselectedPaymentMethod(data) + data.remove(PRE_SELECTED_PAYMENT_METHOD_KEY) + setResult(Activity.RESULT_OK, Intent().putExtras(data)) + finish() + } + + override fun showBackupNotification(walletAddress: String) { + BackupNotificationUtils.showBackupNotification(this, walletAddress) + } + + override fun finish(bundle: Bundle) { + if (bundle.getInt(AppcoinsBillingBinder.RESPONSE_CODE) == AppcoinsBillingBinder.RESULT_OK) { + presenter.handleBackupNotifications(bundle) + presenter.handlePerkNotifications(bundle) + } else { + finishActivity(bundle) + } + } + + override fun finishWithError() { + setResult(Activity.RESULT_CANCELED) + finish() + } + + override fun close(bundle: Bundle?) { + val intent = Intent() + bundle?.let { intent.putExtras(bundle) } + setResult(Activity.RESULT_CANCELED, intent) + finish() + } + + override fun navigateToWebViewAuthorization(url: String) { + startActivityForResult(WebViewActivity.newIntent(this, url), WEB_VIEW_REQUEST_CODE) + } + + override fun showWalletValidation(@StringRes error: Int) { + fragment_container.visibility = View.GONE + val intent = WalletValidationDialogActivity.newIntent(this, error) + .apply { intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP } + startActivityForResult(intent, WALLET_VALIDATION_REQUEST_CODE) + } + + override fun showOnChain(amount: BigDecimal, isBds: Boolean, bonus: String, + gamificationLevel: Int) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, OnChainBuyFragment.newInstance(createBundle(amount), + intent.data!!.toString(), isBds, transaction, bonus, gamificationLevel)) + .commit() + } + + override fun showAdyenPayment(amount: BigDecimal, currency: String?, isBds: Boolean, + paymentType: PaymentType, bonus: String?, isPreselected: Boolean, + iconUrl: String?, gamificationLevel: Int) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + AdyenPaymentFragment.newInstance(transaction!!.type, paymentType, transaction!!.domain, + getOrigin(isBds), intent.dataString, transaction!!.amount(), amount, currency, + bonus, isPreselected, gamificationLevel, getSkuDescription())) + .commit() + } + + override fun showAppcoinsCreditsPayment(appcAmount: BigDecimal, gamificationLevel: Int) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + AppcoinsRewardsBuyFragment.newInstance(appcAmount, transaction!!, intent.data!! + .toString(), isBds, gamificationLevel)) + .commit() + } + + override fun showLocalPayment(domain: String, skuId: String?, originalAmount: String?, + currency: String?, bonus: String?, selectedPaymentMethod: String, + developerAddress: String, type: String, amount: BigDecimal, + callbackUrl: String?, orderReference: String?, payload: String?, + paymentMethodIconUrl: String, paymentMethodLabel: String, + gamificationLevel: Int) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + LocalPaymentFragment.newInstance(domain, skuId, originalAmount, currency, bonus, + selectedPaymentMethod, developerAddress, type, amount, callbackUrl, orderReference, + payload, paymentMethodIconUrl, paymentMethodLabel, gamificationLevel)) + .commit() + } + + override fun showPaymentMethodsView() { + val isDonation = TransactionData.TransactionType.DONATION.name + .equals(transaction?.type, ignoreCase = true) + layout_error.visibility = View.GONE + fragment_container.visibility = View.VISIBLE + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, PaymentMethodsFragment.newInstance(transaction, + getSkuDescription(), isBds, isDonation, developerPayload, uri, + intent.dataString)) + .commit() + } + + override fun showBillingAddress(value: BigDecimal, currency: String, bonus: String, + appcAmount: BigDecimal, targetFragment: Fragment, + shouldStoreCard: Boolean, isStored: Boolean) { + val isDonation = TransactionData.TransactionType.DONATION.name + .equals(transaction?.type, ignoreCase = true) + + val fragment = BillingAddressFragment.newInstance(getSkuDescription(), transaction!!.domain, + appcAmount, bonus, value, currency, isDonation, shouldStoreCard, isStored) + .apply { + setTargetFragment(targetFragment, TopUpActivity.BILLING_ADDRESS_REQUEST_CODE) + } + + supportFragmentManager.beginTransaction() + .add(R.id.fragment_container, fragment) + .addToBackStack(BillingAddressFragment::class.java.simpleName) + .commit() + } + + override fun showShareLinkPayment(domain: String, skuId: String?, originalAmount: String?, + originalCurrency: String?, amount: BigDecimal, type: String, + selectedPaymentMethod: String) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + SharePaymentLinkFragment.newInstance(domain, skuId, originalAmount, originalCurrency, + amount, type, selectedPaymentMethod)) + .commit() + } + + override fun showMergedAppcoins(fiatAmount: BigDecimal, currency: String, bonus: String, + isBds: Boolean, isDonation: Boolean, gamificationLevel: Int) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + MergedAppcoinsFragment.newInstance(fiatAmount, currency, bonus, transaction!!.domain, + getSkuDescription(), transaction!!.amount(), isBds, + isDonation, transaction!!.skuId, transaction!!.type, gamificationLevel, + transaction!!)) + .commit() + } + + override fun showEarnAppcoins(domain: String, skuId: String?, amount: BigDecimal, + type: String) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + EarnAppcoinsFragment.newInstance(domain, skuId, amount, type)) + .commit() + } + + override fun showUpdateRequiredView() { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, IabUpdateRequiredFragment()) + .commit() + } + + override fun showError(@StringRes error: Int) { + fragment_container.visibility = View.GONE + layout_error.visibility = View.VISIBLE + error_message.text = getText(error) + } + + override fun getSupportClicks(): Observable = + Observable.merge(RxView.clicks(layout_support_logo), RxView.clicks(layout_support_icn)) + + override fun errorDismisses() = RxView.clicks(error_dismiss) + + override fun launchPerkBonusService(address: String) { + PerkBonusService.buildService(this, address) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + presenter.onSaveInstance(outState) + } + + private fun getOrigin(isBds: Boolean): String? { + return if (transaction!!.origin == null) { + if (isBds) BDS else null + } else { + transaction!!.origin + } + } + + private fun createBundle(amount: BigDecimal): Bundle { + return Bundle().apply { + putSerializable(TRANSACTION_AMOUNT, amount) + putString(APP_PACKAGE, transaction!!.domain) + putString(PRODUCT_NAME, intent.extras!!.getString(PRODUCT_NAME)) + putString(TRANSACTION_DATA, intent.dataString) + putString(DEVELOPER_PAYLOAD, transaction!!.payload) + } + } + + fun isBds() = intent.getBooleanExtra(EXTRA_BDS_IAP, false) + + override fun navigateToUri(url: String) { + navigateToWebViewAuthorization(url) + } + + override fun uriResults() = results + + override fun launchIntent(intent: Intent) { + startActivity(intent) + } + + override fun lockRotation() { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED + } + + override fun unlockRotation() { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } + + override fun backButtonPress() = backButtonPress!! + + override fun successWebViewResult(data: Uri?) { + results!!.accept(Objects.requireNonNull(data, "Intent data cannot be null!")) + } + + override fun authenticationResult(success: Boolean) { + authenticationResultSubject?.onNext(success) + } + + override fun onPause() { + presenter.stop() + super.onPause() + } + + override fun onDestroy() { + backButtonPress = null + super.onDestroy() + } + + private fun getSkuDescription(): String { + return when { + transaction?.productName.isNullOrEmpty() + .not() -> transaction?.productName!! + transaction != null && transaction!!.skuId.isNullOrEmpty() + .not() -> transaction!!.skuId + else -> "" + } + } + + override fun showAuthenticationActivity() { + val intent = AuthenticationPromptActivity.newIntent(this) + .apply { intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP } + startActivityForResult(intent, AUTHENTICATION_REQUEST_CODE) + } + + override fun onAuthenticationResult(): Observable { + return authenticationResultSubject!! + } + + companion object { + + const val BILLING_ADDRESS_REQUEST_CODE = 1236 + const val BILLING_ADDRESS_SUCCESS_CODE = 1000 + const val BILLING_ADDRESS_CANCEL_CODE = 1001 + const val URI = "uri" + const val RESPONSE_CODE = "RESPONSE_CODE" + const val RESULT_USER_CANCELED = 1 + const val APP_PACKAGE = "app_package" + const val TRANSACTION_EXTRA = "transaction_extra" + const val PRODUCT_NAME = "product_name" + const val TRANSACTION_DATA = "transaction_data" + const val TRANSACTION_HASH = "transaction_hash" + const val TRANSACTION_AMOUNT = "transaction_amount" + const val DEVELOPER_PAYLOAD = "developer_payload" + const val BDS = "BDS" + const val WEB_VIEW_REQUEST_CODE = 1234 + const val BLOCKED_WARNING_REQUEST_CODE = 12345 + const val WALLET_VALIDATION_REQUEST_CODE = 12346 + const val AUTHENTICATION_REQUEST_CODE = 33 + const val IS_BDS_EXTRA = "is_bds_extra" + const val ERROR_MESSAGE = "error_message" + + @JvmStatic + fun newIntent(activity: Activity, previousIntent: Intent, transaction: TransactionBuilder, + isBds: Boolean?, developerPayload: String?): Intent { + return Intent(activity, IabActivity::class.java) + .apply { + data = previousIntent.data + if (previousIntent.extras != null) { + putExtras(previousIntent.extras!!) + } + putExtra(TRANSACTION_EXTRA, transaction) + putExtra(IS_BDS_EXTRA, isBds) + putExtra(DEVELOPER_PAYLOAD, developerPayload) + putExtra(URI, data!!.toString()) + putExtra(APP_PACKAGE, transaction.domain) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabInteract.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabInteract.kt new file mode 100644 index 00000000000..51da133d26c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabInteract.kt @@ -0,0 +1,48 @@ +package com.asfoundation.wallet.ui.iab + +import com.appcoins.wallet.gamification.Gamification +import com.asfoundation.wallet.backup.NotificationNeeded +import com.asfoundation.wallet.interact.AutoUpdateInteract +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import io.reactivex.Single + +class IabInteract(private val inAppPurchaseInteractor: InAppPurchaseInteractor, + private val autoUpdateInteract: AutoUpdateInteract, + private val supportInteractor: SupportInteractor, + private val gamificationRepository: Gamification, + private val walletBlockedInteract: WalletBlockedInteract) { + + companion object { + const val PRE_SELECTED_PAYMENT_METHOD_KEY = "PRE_SELECTED_PAYMENT_METHOD_KEY" + } + + fun showSupport() = supportInteractor.displayChatScreen() + + fun hasPreSelectedPaymentMethod() = inAppPurchaseInteractor.hasPreSelectedPaymentMethod() + + fun getPreSelectedPaymentMethod(): String = inAppPurchaseInteractor.preSelectedPaymentMethod + + fun getWalletAddress(): Single = inAppPurchaseInteractor.walletAddress + + fun getAutoUpdateModel(invalidateCache: Boolean = true) = + autoUpdateInteract.getAutoUpdateModel(invalidateCache) + + fun isHardUpdateRequired(blackList: List, updateVersionCode: Int, updateMinSdk: Int) = + autoUpdateInteract.isHardUpdateRequired(blackList, updateVersionCode, updateMinSdk) + + fun registerUser() = + inAppPurchaseInteractor.walletAddress.flatMap { address -> + gamificationRepository.getUserStats(address) + .doOnSuccess { supportInteractor.registerUser(it.level, address) } + } + + fun savePreSelectedPaymentMethod(paymentMethod: String) { + inAppPurchaseInteractor.savePreSelectedPaymentMethod(paymentMethod) + } + + fun incrementAndValidateNotificationNeeded(): Single = + inAppPurchaseInteractor.incrementAndValidateNotificationNeeded() + + fun isWalletBlocked() = walletBlockedInteract.isWalletBlocked() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabPresenter.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabPresenter.java deleted file mode 100644 index 4ee88abe2b4..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabPresenter.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.asfoundation.wallet.ui.iab; - -import android.util.Log; -import com.asfoundation.wallet.repository.PaymentTransaction; -import com.asfoundation.wallet.util.UnknownTokenException; -import io.reactivex.Completable; -import io.reactivex.Scheduler; -import io.reactivex.disposables.CompositeDisposable; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; - -/** - * Created by trinkes on 13/03/2018. - */ - -public class IabPresenter { - private static final String TAG = IabPresenter.class.getSimpleName(); - private final IabView view; - private final InAppPurchaseInteractor inAppPurchaseInteractor; - private final Scheduler viewScheduler; - private final CompositeDisposable disposables; - - public IabPresenter(IabView view, InAppPurchaseInteractor inAppPurchaseInteractor, - Scheduler viewScheduler, CompositeDisposable disposables) { - this.view = view; - this.inAppPurchaseInteractor = inAppPurchaseInteractor; - this.viewScheduler = viewScheduler; - this.disposables = disposables; - } - - public void present(String uriString, String appPackage, String productName) { - disposables.add(inAppPurchaseInteractor.parseTransaction(uriString) - .observeOn(viewScheduler) - .subscribe(transactionBuilder -> view.setup(transactionBuilder), this::showError)); - - disposables.add(view.getCancelClick() - .subscribe(click -> close())); - - disposables.add(view.getOkErrorClick() - .flatMapSingle(__ -> inAppPurchaseInteractor.parseTransaction(uriString)) - .subscribe(click -> showBuy(), throwable -> close())); - - disposables.add(view.getBuyClick() - .flatMapCompletable(uri -> inAppPurchaseInteractor.send(uri, appPackage, productName) - .observeOn(viewScheduler) - .doOnError(this::showError)) - .retry() - .subscribe()); - - disposables.add(inAppPurchaseInteractor.getTransactionState(uriString) - .observeOn(viewScheduler) - .flatMapCompletable(this::showPendingTransaction) - .subscribe(() -> { - }, throwable -> throwable.printStackTrace())); - } - - private void showBuy() { - view.showBuy(); - } - - private void close() { - view.close(); - } - - private void showError(@Nullable Throwable throwable) { - if (throwable != null) { - throwable.printStackTrace(); - } - if (throwable instanceof UnknownTokenException) { - view.showWrongNetworkError(); - } else { - view.showError(); - } - } - - private Completable showPendingTransaction(PaymentTransaction transaction) { - Log.d(TAG, "present: " + transaction); - switch (transaction.getState()) { - case COMPLETED: - return Completable.fromAction(view::showTransactionCompleted) - .andThen(Completable.timer(1, TimeUnit.SECONDS)) - .andThen(Completable.fromAction(() -> { - view.finish(transaction.getBuyHash()); - })) - .andThen(inAppPurchaseInteractor.remove(transaction.getUri())); - case NO_FUNDS: - return Completable.fromAction(() -> view.showNoFundsError()) - .andThen(inAppPurchaseInteractor.remove(transaction.getUri())); - case WRONG_NETWORK: - case UNKNOWN_TOKEN: - return Completable.fromAction(() -> view.showWrongNetworkError()) - .andThen(inAppPurchaseInteractor.remove(transaction.getUri())); - case NO_TOKENS: - return Completable.fromAction(() -> view.showNoTokenFundsError()) - .andThen(inAppPurchaseInteractor.remove(transaction.getUri())); - case NO_ETHER: - return Completable.fromAction(() -> view.showNoEtherFundsError()) - .andThen(inAppPurchaseInteractor.remove(transaction.getUri())); - case NO_INTERNET: - return Completable.fromAction(() -> view.showNoNetworkError()) - .andThen(inAppPurchaseInteractor.remove(transaction.getUri())); - case NONCE_ERROR: - return Completable.fromAction(() -> view.showNonceError()) - .andThen(inAppPurchaseInteractor.remove(transaction.getUri())); - case PENDING: - case APPROVING: - case APPROVED: - return Completable.fromAction(view::showApproving); - case BUYING: - case BOUGHT: - return Completable.fromAction(view::showBuying); - default: - case ERROR: - return Completable.fromAction(() -> showError(null)) - .andThen(inAppPurchaseInteractor.remove(transaction.getUri())); - } - } - - public void stop() { - disposables.clear(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabPresenter.kt new file mode 100644 index 00000000000..f3bd94c1427 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabPresenter.kt @@ -0,0 +1,222 @@ +package com.asfoundation.wallet.ui.iab + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.annotation.StringRes +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.ui.AuthenticationPromptActivity +import com.asfoundation.wallet.ui.iab.IabInteract.Companion.PRE_SELECTED_PAYMENT_METHOD_KEY +import io.reactivex.Completable +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.TimeUnit + +class IabPresenter(private val view: IabView, + private val networkScheduler: Scheduler, + private val viewScheduler: Scheduler, + private val disposable: CompositeDisposable, + private val billingAnalytics: BillingAnalytics, + private val iabInteract: IabInteract, + private val logger: Logger, + private val transaction: TransactionBuilder?) { + + private var firstImpression = true + + companion object { + private val TAG = IabActivity::class.java.name + private const val FIRST_IMPRESSION = "first_impression" + } + + fun present(savedInstanceState: Bundle?) { + savedInstanceState?.let { + firstImpression = it.getBoolean(FIRST_IMPRESSION, firstImpression) + } + if (savedInstanceState == null) { + handlePurchaseStartAnalytics(transaction) + view.showPaymentMethodsView() + } + } + + fun onResume() { + handleAutoUpdate() + handleUserRegistration() + handleSupportClicks() + handleErrorDismisses() + } + + private fun handleErrorDismisses() { + disposable.add(view.errorDismisses() + .doOnNext { view.close(Bundle()) } + .subscribe({ }, { view.close(Bundle()) })) + } + + private fun handleSupportClicks() { + disposable.add(view.getSupportClicks() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .doOnNext { iabInteract.showSupport() } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleWalletBlockedCheck(@StringRes error: Int) { + disposable.add(iabInteract.isWalletBlocked() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { + if (it) view.showError(error) + else view.showPaymentMethodsView() + } + .subscribe({}, { handleError(it) }) + ) + } + + private fun handleError(throwable: Throwable) { + logger.log(TAG, throwable) + view.finishWithError() + } + + fun handlePerkNotifications(bundle: Bundle) { + disposable.add(iabInteract.getWalletAddress() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { + view.launchPerkBonusService(it) + view.finishActivity(bundle) + } + .doOnError { view.finishActivity(bundle) } + .subscribe({}, { it.printStackTrace() })) + } + + fun handleBackupNotifications(bundle: Bundle) { + disposable.add(iabInteract.incrementAndValidateNotificationNeeded() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { notificationNeeded -> + if (notificationNeeded.isNeeded) { + view.showBackupNotification(notificationNeeded.walletAddress) + } + view.finishActivity(bundle) + } + .doOnError { view.finish(bundle) } + .subscribe({ }, { it.printStackTrace() }) + ) + } + + fun handlePurchaseStartAnalytics(transaction: TransactionBuilder?) { + disposable.add(Completable.fromAction { + if (firstImpression) { + if (iabInteract.hasPreSelectedPaymentMethod()) { + billingAnalytics.sendPurchaseStartEvent(transaction?.domain, transaction?.skuId, + transaction?.amount() + .toString(), iabInteract.getPreSelectedPaymentMethod(), + transaction?.type, BillingAnalytics.RAKAM_PRESELECTED_PAYMENT_METHOD) + } else { + billingAnalytics.sendPurchaseStartWithoutDetailsEvent(transaction?.domain, + transaction?.skuId, transaction?.amount() + .toString(), transaction?.type, + BillingAnalytics.RAKAM_PAYMENT_METHOD) + } + firstImpression = false + } + } + .subscribeOn(networkScheduler) + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleAutoUpdate() { + disposable.add(iabInteract.getAutoUpdateModel() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .filter { + iabInteract.isHardUpdateRequired(it.blackList, it.updateVersionCode, it.updateMinSdk) + } + .doOnSuccess { view.showUpdateRequiredView() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleUserRegistration() { + disposable.add(iabInteract.registerUser() + .subscribeOn(networkScheduler) + .subscribe({}, { it.printStackTrace() })) + } + + fun stop() = disposable.clear() + + fun onSaveInstance(outState: Bundle) { + outState.putBoolean(FIRST_IMPRESSION, firstImpression) + } + + fun savePreselectedPaymentMethod(bundle: Bundle) { + bundle.getString(PRE_SELECTED_PAYMENT_METHOD_KEY) + ?.let { + iabInteract.savePreSelectedPaymentMethod(it) + } + } + + private fun sendPayPalConfirmationEvent(action: String) { + billingAnalytics.sendPaymentConfirmationEvent(transaction?.domain, transaction?.skuId, + transaction?.amount() + .toString(), "paypal", + transaction?.type, action) + } + + private fun sendPaypalUrlEvent(data: Intent) { + val amountString = transaction?.amount() + .toString() + billingAnalytics.sendPaypalUrlEvent(transaction?.domain, transaction?.skuId, + amountString, "PAYPAL", getQueryParameter(data, "type"), + getQueryParameter(data, "resultCode"), data.dataString) + } + + private fun getQueryParameter(data: Intent, parameter: String): String? { + return Uri.parse(data.dataString) + .getQueryParameter(parameter) + } + + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + IabActivity.WEB_VIEW_REQUEST_CODE -> handleWebViewResult(resultCode, data) + IabActivity.WALLET_VALIDATION_REQUEST_CODE -> handleWalletValidationResult(data) + IabActivity.AUTHENTICATION_REQUEST_CODE -> handleAuthenticationResult(resultCode) + } + } + + private fun handleWebViewResult(resultCode: Int, data: Intent?) { + if (resultCode == WebViewActivity.FAIL) { + if (data?.dataString?.contains("codapayments") != true) { + sendPayPalConfirmationEvent("cancel") + } + view.showPaymentMethodsView() + } else if (resultCode == WebViewActivity.SUCCESS) { + if (data?.scheme?.contains("adyencheckout") == true) { + sendPaypalUrlEvent(data) + if (getQueryParameter(data, "resultCode") == "cancelled") + sendPayPalConfirmationEvent("cancel") + else + sendPayPalConfirmationEvent("buy") + } + view.successWebViewResult(data!!.data) + } + } + + private fun handleWalletValidationResult(data: Intent?) { + var errorMessage = data?.getIntExtra(IabActivity.ERROR_MESSAGE, 0) + if (errorMessage == null || errorMessage == 0) { + errorMessage = R.string.unknown_error + } + handleWalletBlockedCheck(errorMessage) + } + + private fun handleAuthenticationResult(resultCode: Int) { + if (resultCode == AuthenticationPromptActivity.RESULT_OK) { + view.authenticationResult(true) + } else if (resultCode == AuthenticationPromptActivity.RESULT_CANCELED) { + view.authenticationResult(false) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabUpdateRequiredFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabUpdateRequiredFragment.kt new file mode 100644 index 00000000000..0d5e8b53fe3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabUpdateRequiredFragment.kt @@ -0,0 +1,66 @@ +package com.asfoundation.wallet.ui.iab + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.interact.AutoUpdateInteract +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.google.android.material.snackbar.Snackbar +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.dialog_buy_buttons.view.* +import kotlinx.android.synthetic.main.iab_update_required_layout.* +import javax.inject.Inject + +class IabUpdateRequiredFragment : BasePageViewFragment(), IabUpdateRequiredView { + + private lateinit var presenter: IabUpdateRequiredPresenter + private lateinit var iabView: IabView + + @Inject + lateinit var autoUpdateInteract: AutoUpdateInteract + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = IabUpdateRequiredPresenter(this, CompositeDisposable(), autoUpdateInteract) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check(context is IabView) { "IabUpdateRequired fragment must be attached to IAB activity" } + iabView = context + } + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + update_dialog_buttons.buy_button.text = getString(R.string.update_button) + update_dialog_buttons.cancel_button.text = getString(R.string.cancel_button) + presenter.present() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.iab_update_required_layout, container, false) + } + + override fun navigateToIntent(intent: Intent) = startActivity(intent) + + override fun updateClick() = RxView.clicks(update_dialog_buttons.buy_button) + + override fun cancelClick() = RxView.clicks(update_dialog_buttons.cancel_button) + + override fun close() = iabView.close(Bundle()) + + override fun showError() = + Snackbar.make(main_layout, R.string.unknown_error, Snackbar.LENGTH_SHORT) + + override fun onDestroyView() { + super.onDestroyView() + presenter.stop() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabUpdateRequiredPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabUpdateRequiredPresenter.kt new file mode 100644 index 00000000000..10ebc24f165 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabUpdateRequiredPresenter.kt @@ -0,0 +1,36 @@ +package com.asfoundation.wallet.ui.iab + +import com.asfoundation.wallet.interact.AutoUpdateInteract +import io.reactivex.disposables.CompositeDisposable + +class IabUpdateRequiredPresenter(private val view: IabUpdateRequiredView, + private val disposables: CompositeDisposable, + private val autoUpdateInteract: AutoUpdateInteract) { + + fun present() { + handleUpdateClick() + handleCancelClick() + } + + private fun handleCancelClick() { + disposables.add(view.cancelClick() + .doOnNext { view.close() } + .subscribe()) + } + + private fun handleUpdateClick() { + disposables.add(view.updateClick() + .doOnNext { view.navigateToIntent(autoUpdateInteract.buildUpdateIntent()) } + .subscribe({}, { handleError(it) })) + } + + private fun handleError(throwable: Throwable) { + throwable.printStackTrace() + view.showError() + } + + fun stop() { + disposables.clear() + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabUpdateRequiredView.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabUpdateRequiredView.kt new file mode 100644 index 00000000000..1ebb1868af1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabUpdateRequiredView.kt @@ -0,0 +1,17 @@ +package com.asfoundation.wallet.ui.iab + +import android.content.Intent +import com.google.android.material.snackbar.Snackbar +import io.reactivex.Observable + +interface IabUpdateRequiredView { + + fun navigateToIntent(intent: Intent) + + fun updateClick(): Observable + + fun cancelClick(): Observable + + fun close() + fun showError(): Snackbar +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabView.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabView.java deleted file mode 100644 index 2ce02dcba20..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabView.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.asfoundation.wallet.ui.iab; - -import com.asfoundation.wallet.entity.TransactionBuilder; -import io.reactivex.Observable; - -/** - * Created by trinkes on 13/03/2018. - */ - -public interface IabView { - Observable getBuyClick(); - - Observable getCancelClick(); - - Observable getOkErrorClick(); - - void finish(String hash); - - void showLoading(); - - void showError(); - - void setup(TransactionBuilder transactionBuilder); - - void close(); - - void showTransactionCompleted(); - - void showBuy(); - - void showWrongNetworkError(); - - void showNoNetworkError(); - - void showApproving(); - - void showBuying(); - - void showNonceError(); - - void showNoTokenFundsError(); - - void showNoEtherFundsError(); - - void showNoFundsError(); -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/IabView.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabView.kt new file mode 100644 index 00000000000..884ca5d8fd2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/IabView.kt @@ -0,0 +1,92 @@ +package com.asfoundation.wallet.ui.iab + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import com.asfoundation.wallet.billing.adyen.PaymentType +import io.reactivex.Observable +import java.math.BigDecimal + +/** + * Created by franciscocalado on 20/07/2018. + */ + +interface IabView { + + fun disableBack() + + fun enableBack() + + fun finish(bundle: Bundle) + + fun finishWithError() + + fun navigateBack() + + fun close(bundle: Bundle?) + + fun navigateToWebViewAuthorization(url: String) + + fun showOnChain(amount: BigDecimal, isBds: Boolean, bonus: String, gamificationLevel: Int) + + fun showAdyenPayment(amount: BigDecimal, currency: String?, isBds: Boolean, + paymentType: PaymentType, bonus: String?, isPreselected: Boolean, + iconUrl: String?, gamificationLevel: Int) + + fun showAppcoinsCreditsPayment(appcAmount: BigDecimal, gamificationLevel: Int) + + fun showLocalPayment(domain: String, skuId: String?, originalAmount: String?, currency: String?, + bonus: String?, selectedPaymentMethod: String, developerAddress: String, + type: String, amount: BigDecimal, callbackUrl: String?, + orderReference: String?, payload: String?, paymentMethodIconUrl: String, + paymentMethodLabel: String, gamificationLevel: Int) + + fun showPaymentMethodsView() + + fun showShareLinkPayment(domain: String, skuId: String?, originalAmount: String?, + originalCurrency: String?, amount: BigDecimal, type: String, + selectedPaymentMethod: String) + + fun showMergedAppcoins(fiatAmount: BigDecimal, currency: String, bonus: String, + isBds: Boolean, isDonation: Boolean, gamificationLevel: Int) + + fun showBillingAddress(value: BigDecimal, currency: String, bonus: String, + appcAmount: BigDecimal, targetFragment: Fragment, shouldStoreCard: Boolean, + isStored: Boolean) + + fun lockRotation() + + fun unlockRotation() + + fun showEarnAppcoins(domain: String, skuId: String?, amount: BigDecimal, type: String) + + fun launchIntent(intent: Intent) + + fun showUpdateRequiredView() + + fun finishActivity(data: Bundle) + + fun showBackupNotification(walletAddress: String) + + fun showWalletValidation(@StringRes error: Int) + + fun showError(@StringRes error: Int) + + fun getSupportClicks(): Observable + + fun errorDismisses(): Observable + + fun launchPerkBonusService(address: String) + + fun showAuthenticationActivity() + + fun onAuthenticationResult(): Observable + + fun backButtonPress(): Observable + + fun successWebViewResult(data: Uri?) + + fun authenticationResult(success: Boolean) +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/InAppPurchaseInteractor.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/InAppPurchaseInteractor.java index 9bc216e0923..f722d538abf 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/iab/InAppPurchaseInteractor.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/InAppPurchaseInteractor.java @@ -1,71 +1,511 @@ package com.asfoundation.wallet.ui.iab; -import com.asfoundation.wallet.entity.GasSettings; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import com.appcoins.wallet.appcoins.rewards.AppcoinsRewards; +import com.appcoins.wallet.bdsbilling.Billing; +import com.appcoins.wallet.bdsbilling.mappers.ExternalBillingSerializer; +import com.appcoins.wallet.bdsbilling.repository.entity.FeeEntity; +import com.appcoins.wallet.bdsbilling.repository.entity.FeeType; +import com.appcoins.wallet.bdsbilling.repository.entity.Gateway; +import com.appcoins.wallet.bdsbilling.repository.entity.PaymentMethodEntity; +import com.appcoins.wallet.bdsbilling.repository.entity.Purchase; +import com.appcoins.wallet.bdsbilling.repository.entity.Transaction; +import com.appcoins.wallet.billing.BillingMessagesMapper; +import com.appcoins.wallet.billing.repository.entity.TransactionData; +import com.asf.wallet.BuildConfig; +import com.asf.wallet.R; +import com.asfoundation.wallet.backup.BackupInteractContract; +import com.asfoundation.wallet.backup.NotificationNeeded; import com.asfoundation.wallet.entity.TransactionBuilder; -import com.asfoundation.wallet.interact.FetchGasSettingsInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.repository.InAppPurchaseService; -import com.asfoundation.wallet.repository.PaymentTransaction; -import com.asfoundation.wallet.util.TransferParser; +import com.asfoundation.wallet.interact.GetDefaultWalletBalanceInteract; +import com.asfoundation.wallet.util.BalanceUtils; import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; import java.util.List; +import java.util.concurrent.TimeUnit; public class InAppPurchaseInteractor { - public static final double GAS_PRICE_MULTIPLIER = 1.25; - private final InAppPurchaseService inAppPurchaseService; - private final FindDefaultWalletInteract defaultWalletInteract; - private final FetchGasSettingsInteract gasSettingsInteract; - private final BigDecimal paymentGasLimit; - private final TransferParser parser; - public InAppPurchaseInteractor(InAppPurchaseService inAppPurchaseService, - FindDefaultWalletInteract defaultWalletInteract, FetchGasSettingsInteract gasSettingsInteract, - BigDecimal paymentGasLimit, TransferParser parser) { - this.inAppPurchaseService = inAppPurchaseService; - this.defaultWalletInteract = defaultWalletInteract; - this.gasSettingsInteract = gasSettingsInteract; - this.paymentGasLimit = paymentGasLimit; - this.parser = parser; + public static final String PRE_SELECTED_PAYMENT_METHOD_KEY = "PRE_SELECTED_PAYMENT_METHOD_KEY"; + private static final String LOCAL_PAYMENT_METHOD_KEY = "LOCAL_PAYMENT_METHOD_KEY"; + private static final String LAST_USED_PAYMENT_METHOD_KEY = "LAST_USED_PAYMENT_METHOD_KEY"; + private static final String APPC_ID = "appcoins"; + private static final String CREDITS_ID = "appcoins_credits"; + private static final long EARN_APPCOINS_APTOIDE_VERCODE = 9961; + private final AsfInAppPurchaseInteractor asfInAppPurchaseInteractor; + private final BdsInAppPurchaseInteractor bdsInAppPurchaseInteractor; + private final ExternalBillingSerializer billingSerializer; + private final AppcoinsRewards appcoinsRewards; + private final Billing billing; + private final SharedPreferences sharedPreferences; + private final PackageManager packageManager; + private final BackupInteractContract backupInteract; + + public InAppPurchaseInteractor(AsfInAppPurchaseInteractor asfInAppPurchaseInteractor, + BdsInAppPurchaseInteractor bdsInAppPurchaseInteractor, + ExternalBillingSerializer billingSerializer, AppcoinsRewards appcoinsRewards, Billing billing, + SharedPreferences sharedPreferences, PackageManager packageManager, + BackupInteractContract backupInteract) { + this.asfInAppPurchaseInteractor = asfInAppPurchaseInteractor; + this.bdsInAppPurchaseInteractor = bdsInAppPurchaseInteractor; + this.billingSerializer = billingSerializer; + this.appcoinsRewards = appcoinsRewards; + this.billing = billing; + this.sharedPreferences = sharedPreferences; + this.packageManager = packageManager; + this.backupInteract = backupInteract; } - public Single parseTransaction(String uri) { - return parser.parse(uri); + public Single incrementAndValidateNotificationNeeded() { + return asfInAppPurchaseInteractor.getWalletAddress() + .flatMap(walletAddress -> backupInteract.updateWalletPurchasesCount(walletAddress) + .andThen(shouldShowSystemNotification(walletAddress).map( + needed -> new NotificationNeeded(needed, walletAddress)))); } - public Completable send(String uri, String packageName, String productName) { - return buildPaymentTransaction(uri, packageName, productName).flatMapCompletable( - paymentTransaction -> inAppPurchaseService.send(paymentTransaction.getUri(), - paymentTransaction)); + private Single shouldShowSystemNotification(String walletAddress) { + return Single.create(emitter -> { + boolean shouldShow = backupInteract.shouldShowSystemNotification(walletAddress); + emitter.onSuccess(shouldShow); + }); } - public Observable getTransactionState(String uri) { - return inAppPurchaseService.getTransactionState(uri); + public Single parseTransaction(String uri, boolean isBds) { + if (isBds) { + return bdsInAppPurchaseInteractor.parseTransaction(uri); + } else { + return asfInAppPurchaseInteractor.parseTransaction(uri); + } } - public Completable remove(String uri) { - return inAppPurchaseService.remove(uri); + public Completable send(String uri, AsfInAppPurchaseInteractor.TransactionType transactionType, + String packageName, String productName, String developerPayload, boolean isBds) { + if (isBds) { + return bdsInAppPurchaseInteractor.send(uri, transactionType, packageName, productName, + developerPayload); + } else { + return asfInAppPurchaseInteractor.send(uri, transactionType, packageName, productName, + developerPayload); + } + } + + Completable resume(String uri, AsfInAppPurchaseInteractor.TransactionType transactionType, + String packageName, String productName, String developerPayload, boolean isBds) { + if (isBds) { + return bdsInAppPurchaseInteractor.resume(uri, transactionType, packageName, productName, + developerPayload); + } else { + return Completable.error(new UnsupportedOperationException("Asf doesn't support resume.")); + } + } + + Observable getTransactionState(String uri) { + return Observable.merge(asfInAppPurchaseInteractor.getTransactionState(uri), + bdsInAppPurchaseInteractor.getTransactionState(uri)); } - private Single buildPaymentTransaction(String uri, String packageName, - String productName) { - return Single.zip(parseTransaction(uri), defaultWalletInteract.find(), - (transaction, wallet) -> transaction.fromAddress(wallet.address)) - .flatMap(transactionBuilder -> gasSettingsInteract.fetch(true) - .map(gasSettings -> transactionBuilder.gasSettings( - new GasSettings(gasSettings.gasPrice.multiply(new BigDecimal(GAS_PRICE_MULTIPLIER)), - paymentGasLimit)))) - .map(transactionBuilder -> new PaymentTransaction(uri, transactionBuilder, packageName, - productName)); + public Completable remove(String uri) { + return asfInAppPurchaseInteractor.remove(uri) + .andThen(bdsInAppPurchaseInteractor.remove(uri)); } public void start() { - inAppPurchaseService.start(); + asfInAppPurchaseInteractor.start(); + bdsInAppPurchaseInteractor.start(); + } + + public Observable> getAll() { + return Observable.merge(asfInAppPurchaseInteractor.getAll(), + bdsInAppPurchaseInteractor.getAll()); + } + + List getTopUpChannelSuggestionValues(BigDecimal price) { + return bdsInAppPurchaseInteractor.getTopUpChannelSuggestionValues(price); + } + + public Single getWalletAddress() { + return asfInAppPurchaseInteractor.getWalletAddress(); + } + + Single getCurrentPaymentStep(String packageName, + TransactionBuilder transactionBuilder) { + return asfInAppPurchaseInteractor.getCurrentPaymentStep(packageName, transactionBuilder); + } + + public Single convertToFiat(double appcValue, String currency) { + return asfInAppPurchaseInteractor.convertToFiat(appcValue, currency); + } + + public Single convertToLocalFiat(double appcValue) { + return asfInAppPurchaseInteractor.convertToLocalFiat(appcValue); + } + + public BillingMessagesMapper getBillingMessagesMapper() { + return bdsInAppPurchaseInteractor.getBillingMessagesMapper(); + } + + private Single getCompletedPurchase(String packageName, String productName) { + return bdsInAppPurchaseInteractor.getCompletedPurchase(packageName, productName); + } + + Single getCompletedPurchase(Payment transaction, boolean isBds) { + return parseTransaction(transaction.getUri(), isBds).flatMap(transactionBuilder -> { + if (isBds && transactionBuilder.getType() + .equalsIgnoreCase(TransactionData.TransactionType.INAPP.name())) { + return getCompletedPurchase(transaction.getPackageName(), transaction.getProductId()).map( + purchase -> mapToBdsPayment(transaction, purchase)) + .observeOn(AndroidSchedulers.mainThread()) + .flatMap(payment -> remove(transaction.getUri()).toSingleDefault(payment)); + } else { + return Single.fromCallable(() -> transaction) + .flatMap(bundle -> remove(transaction.getUri()).toSingleDefault(bundle)); + } + }); + } + + private Payment mapToBdsPayment(Payment transaction, Purchase purchase) { + return new Payment(transaction.getUri(), transaction.getStatus(), purchase.getUid(), + purchase.getSignature() + .getValue(), billingSerializer.serializeSignatureData(purchase), + transaction.getOrderReference(), transaction.getErrorCode(), transaction.getErrorMessage()); + } + + public Single isWalletFromBds(String packageName, String wallet) { + if (packageName == null) { + return Single.just(false); + } + return bdsInAppPurchaseInteractor.getWallet(packageName) + .map(wallet::equalsIgnoreCase) + .onErrorReturn(throwable -> false); + } + + private Single> getFilteredGateways(TransactionBuilder transactionBuilder) { + return Single.zip(getRewardsBalance(), hasAppcoinsFunds(transactionBuilder), + (creditsBalance, hasAppcoinsFunds) -> getNewPaymentGateways(creditsBalance, + hasAppcoinsFunds, transactionBuilder.amount())); + } + + public Single hasAppcoinsFunds(TransactionBuilder transaction) { + return asfInAppPurchaseInteractor.isAppcoinsPaymentReady(transaction); + } + + public Single getBalanceState( + TransactionBuilder transaction) { + return asfInAppPurchaseInteractor.getAppcoinsBalanceState(transaction); + } + + private List getNewPaymentGateways(BigDecimal creditsBalance, + Boolean hasAppcoinsFunds, BigDecimal amount) { + List list = new LinkedList<>(); + + if (creditsBalance.compareTo(amount) >= 0) { + list.add(Gateway.Name.appcoins_credits); + } + + if (hasAppcoinsFunds) { + list.add(Gateway.Name.appcoins); + } + + list.add(Gateway.Name.adyen_v2); + + return list; + } + + private Single getRewardsBalance() { + return appcoinsRewards.getBalance() + .map(BalanceUtils::weiToEth); + } + + private Single> getAvailablePaymentMethods( + TransactionBuilder transaction, List paymentMethods) { + return getFilteredGateways(transaction).map( + filteredGateways -> removeUnavailable(paymentMethods, filteredGateways)); + } + + public Observable getTransaction(String uid) { + return Observable.interval(0, 5, TimeUnit.SECONDS, Schedulers.io()) + .timeInterval() + .switchMap(longTimed -> billing.getAppcoinsTransaction(uid, Schedulers.io()) + .toObservable()); + } + + Single> getPaymentMethods(TransactionBuilder transaction, + String transactionValue, String currency) { + return bdsInAppPurchaseInteractor.getPaymentMethods(transactionValue, currency) + .flatMap(paymentMethods -> getAvailablePaymentMethods(transaction, paymentMethods).flatMap( + availablePaymentMethods -> Observable.fromIterable(paymentMethods) + .map(paymentMethod -> mapPaymentMethods(paymentMethod, availablePaymentMethods)) + .flatMap(paymentMethod -> retrieveDisableReason(paymentMethod, transaction)) + .toList()) + .map(this::removePaymentMethods)) + .map(this::swapDisabledPositions); + } + + private Observable retrieveDisableReason(PaymentMethod paymentMethod, + TransactionBuilder transaction) { + if (!paymentMethod.isEnabled()) { + if (paymentMethod.getId() + .equals(CREDITS_ID)) { + paymentMethod.setDisabledReason(R.string.purchase_appcoins_credits_noavailable_body); + } else if (paymentMethod.getId() + .equals(APPC_ID)) { + return getAppcDisableReason(transaction).filter(reason -> reason != -1) + .map(reason -> { + paymentMethod.setDisabledReason(reason); + return paymentMethod; + }); + } + } + return Observable.just(paymentMethod); + } + + private Observable getAppcDisableReason(TransactionBuilder transaction) { + return getBalanceState(transaction).map(balanceState -> { + switch (balanceState) { + case NO_ETHER: + return R.string.purchase_no_eth_body; + case NO_TOKEN: + case NO_ETHER_NO_TOKEN: + return R.string.purchase_no_appcoins_body; + case OK: + default: + return -1; + } + }) + .toObservable(); + } + + private List removePaymentMethods(List paymentMethods) { + if (hasFunds(paymentMethods) || !hasRequiredAptoideVersionInstalled()) { + Iterator iterator = paymentMethods.iterator(); + while (iterator.hasNext()) { + PaymentMethod paymentMethod = iterator.next(); + if (paymentMethod.getId() + .equals("earn_appcoins")) { + iterator.remove(); + } + } + } + return paymentMethods; + } + + private boolean hasRequiredAptoideVersionInstalled() { + try { + PackageInfo packageInfo = packageManager.getPackageInfo(BuildConfig.APTOIDE_PKG_NAME, 0); + return packageInfo.versionCode >= EARN_APPCOINS_APTOIDE_VERCODE; + } catch (Exception e) { + return false; + } + } + + private boolean hasFunds(List clonedList) { + for (PaymentMethod paymentMethod : clonedList) { + if ((paymentMethod.getId() + .equals(APPC_ID) && paymentMethod.isEnabled()) + || paymentMethod.getId() + .equals(CREDITS_ID) && paymentMethod.isEnabled()) { + return true; + } + } + return false; + } + + List mergeAppcoins(List paymentMethods) { + PaymentMethod appcMethod = getAppcMethod(paymentMethods); + PaymentMethod creditsMethod = getCreditsMethod(paymentMethods); + if (appcMethod != null && creditsMethod != null) { + return buildMergedList(paymentMethods, appcMethod, creditsMethod); + } + return paymentMethods; + } + + private List buildMergedList(List paymentMethods, + PaymentMethod appcMethod, PaymentMethod creditsMethod) { + List mergedList = new ArrayList<>(); + for (PaymentMethod paymentMethod : paymentMethods) { + if (paymentMethod.getId() + .equals(APPC_ID)) { + String mergedId = "merged_appcoins"; + String mergedLabel = appcMethod.getLabel() + " / " + creditsMethod.getLabel(); + boolean isMergedEnabled = appcMethod.isEnabled() || creditsMethod.isEnabled(); + Integer disableReason = mergeDisableReason(appcMethod, creditsMethod); + mergedList.add(new AppCoinsPaymentMethod(mergedId, mergedLabel, appcMethod.getIconUrl(), + isMergedEnabled, appcMethod.isEnabled(), creditsMethod.isEnabled(), + appcMethod.getLabel(), creditsMethod.getLabel(), creditsMethod.getIconUrl(), + disableReason, appcMethod.getDisabledReason(), creditsMethod.getDisabledReason())); + } else if (!paymentMethod.getId() + .equals(CREDITS_ID)) { + //Don't add the credits method to this list + mergedList.add(paymentMethod); + } + } + return mergedList; + } + + private Integer mergeDisableReason(PaymentMethod appcMethod, PaymentMethod creditsMethod) { + Integer reason = null; + + if (!creditsMethod.isEnabled()) { + if (creditsMethod.getDisabledReason() != -1) { + reason = creditsMethod.getDisabledReason(); + } else { + reason = appcMethod.getDisabledReason(); + } + } else if (!appcMethod.isEnabled()) { + if (appcMethod.getDisabledReason() != -1) { + reason = appcMethod.getDisabledReason(); + } else { + reason = creditsMethod.getDisabledReason(); + } + } + return reason; + } + + private PaymentMethod getCreditsMethod(List paymentMethods) { + for (PaymentMethod paymentMethod : paymentMethods) { + if (paymentMethod.getId() + .equals(CREDITS_ID)) { + return paymentMethod; + } + } + return null; + } + + private PaymentMethod getAppcMethod(List paymentMethods) { + for (PaymentMethod paymentMethod : paymentMethods) { + if (paymentMethod.getId() + .equals(APPC_ID)) { + return paymentMethod; + } + } + return null; + } + + public List swapDisabledPositions(List paymentMethods) { + boolean swapped = false; + if (paymentMethods.size() > 1) { + for (int position = 1; position < paymentMethods.size(); position++) { + if (shouldSwap(paymentMethods, position)) { + Collections.swap(paymentMethods, position, position - 1); + swapped = true; + break; + } + } + if (swapped) { + swapDisabledPositions(paymentMethods); + } + } + return paymentMethods; + } + + private boolean shouldSwap(List paymentMethods, int position) { + return paymentMethods.get(position) + .isEnabled() && !paymentMethods.get(position - 1) + .isEnabled(); + } + + private List removeUnavailable(List paymentMethods, + List filteredGateways) { + List clonedPaymentMethods = new ArrayList<>(paymentMethods); + Iterator iterator = clonedPaymentMethods.iterator(); + + while (iterator.hasNext()) { + PaymentMethodEntity paymentMethod = iterator.next(); + String id = paymentMethod.getId(); + if (id.equals(APPC_ID) && !filteredGateways.contains(Gateway.Name.appcoins)) { + iterator.remove(); + } else if (id.equals(CREDITS_ID) && !filteredGateways.contains( + Gateway.Name.appcoins_credits)) { + iterator.remove(); + } else if (paymentMethod.getGateway() != null && (paymentMethod.getGateway() + .getName() == (Gateway.Name.myappcoins) + || paymentMethod.getGateway() + .getName() == (Gateway.Name.adyen_v2)) && !paymentMethod.isAvailable()) { + iterator.remove(); + } + } + return clonedPaymentMethods; + } + + private PaymentMethod mapPaymentMethods(PaymentMethodEntity paymentMethod, + List availablePaymentMethods) { + for (PaymentMethodEntity availablePaymentMethod : availablePaymentMethods) { + if (paymentMethod.getId() + .equals(availablePaymentMethod.getId())) { + PaymentMethodFee paymentMethodFee = mapPaymentMethodFee(availablePaymentMethod.getFee()); + return new PaymentMethod(paymentMethod.getId(), paymentMethod.getLabel(), + paymentMethod.getIconUrl(), paymentMethodFee, true, null); + } + } + PaymentMethodFee paymentMethodFee = mapPaymentMethodFee(paymentMethod.getFee()); + return new PaymentMethod(paymentMethod.getId(), paymentMethod.getLabel(), + paymentMethod.getIconUrl(), paymentMethodFee, false, null); + } + + private PaymentMethodFee mapPaymentMethodFee(FeeEntity feeEntity) { + if (feeEntity == null) { + return null; + } else { + if (feeEntity.getType() == FeeType.EXACT) { + return new PaymentMethodFee(true, feeEntity.getCost() + .getValue(), feeEntity.getCost() + .getCurrency()); + } else { + return new PaymentMethodFee(false, null, null); + } + } + } + + boolean hasPreSelectedPaymentMethod() { + return sharedPreferences.contains(PRE_SELECTED_PAYMENT_METHOD_KEY); + } + + String getPreSelectedPaymentMethod() { + return sharedPreferences.getString(PRE_SELECTED_PAYMENT_METHOD_KEY, + PaymentMethodsView.PaymentMethodId.APPC_CREDITS.getId()); + } + + boolean hasAsyncLocalPayment() { + return sharedPreferences.contains(LOCAL_PAYMENT_METHOD_KEY); + } + + public void savePreSelectedPaymentMethod(String paymentMethod) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PRE_SELECTED_PAYMENT_METHOD_KEY, paymentMethod); + editor.putString(LAST_USED_PAYMENT_METHOD_KEY, paymentMethod); + editor.apply(); + } + + public void saveAsyncLocalPayment(String paymentMethod) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(LOCAL_PAYMENT_METHOD_KEY, paymentMethod); + editor.apply(); + } + + public void removePreSelectedPaymentMethod() { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(PRE_SELECTED_PAYMENT_METHOD_KEY); + editor.apply(); + } + + public void removeAsyncLocalPayment() { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(LOCAL_PAYMENT_METHOD_KEY); + editor.apply(); } - public Observable> getAll() { - return inAppPurchaseService.getAll(); + String getLastUsedPaymentMethod() { + return sharedPreferences.getString(LAST_USED_PAYMENT_METHOD_KEY, + PaymentMethodsView.PaymentMethodId.CREDIT_CARD.getId()); } } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentAnalytics.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentAnalytics.kt new file mode 100644 index 00000000000..77bb9677197 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentAnalytics.kt @@ -0,0 +1,47 @@ +package com.asfoundation.wallet.ui.iab + +import com.asfoundation.wallet.analytics.FacebookEventLogger.EVENT_REVENUE_CURRENCY +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import java.math.BigDecimal + +class LocalPaymentAnalytics(private val analytics: BillingAnalytics, + private val inAppPurchaseInteractor: InAppPurchaseInteractor, + private val scheduler: Scheduler) { + + + fun sendPaymentMethodDetailsEvent(domain: String, skuId: String?, amount: String, + type: String, paymentId: String) { + analytics.sendPaymentMethodDetailsEvent(domain, skuId, amount, paymentId, type) + } + + + fun sendPaymentEvent(domain: String, skuId: String?, amount: String, + type: String, paymentId: String) { + analytics.sendPaymentEvent(domain, skuId, amount, paymentId, type) + } + + fun sendRevenueEvent(disposable: CompositeDisposable, amount: BigDecimal) { + disposable.add(inAppPurchaseInteractor.convertToFiat(amount.toDouble(), + EVENT_REVENUE_CURRENCY).subscribeOn(scheduler) + .doOnSuccess { fiatValue -> analytics.sendRevenueEvent(fiatValue.amount.toString()) } + .subscribe()) + } + + fun sendPaymentConfirmationEvent(domain: String, skuId: String?, amount: String, type: String, + paymentId: String) { + analytics.sendPaymentConfirmationEvent(domain, skuId, amount, type, paymentId, "buy") + } + + fun sendPaymentConclusionEvent(domain: String, skuId: String?, amount: String, type: String, + paymentId: String) { + analytics.sendPaymentSuccessEvent(domain, skuId, amount, paymentId, type) + } + + fun sendPaymentPendingEvent(domain: String, skuId: String?, amount: String, type: String, + paymentId: String) { + analytics.sendPaymentPendingEvent(domain, skuId, amount, paymentId, type) + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentFragment.kt new file mode 100644 index 00000000000..2b265801827 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentFragment.kt @@ -0,0 +1,485 @@ +package com.asfoundation.wallet.ui.iab + +import android.animation.Animator +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Typeface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.lottie.FontAssetDelegate +import com.airbnb.lottie.TextDelegate +import com.asf.wallet.R +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.navigator.UriNavigator +import com.asfoundation.wallet.ui.iab.LocalPaymentView.ViewState +import com.asfoundation.wallet.ui.iab.LocalPaymentView.ViewState.* +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.fragment_iab_transaction_completed.view.* +import kotlinx.android.synthetic.main.iab_error_layout.view.* +import kotlinx.android.synthetic.main.local_payment_layout.* +import kotlinx.android.synthetic.main.pending_user_payment_view.* +import kotlinx.android.synthetic.main.pending_user_payment_view.view.* +import kotlinx.android.synthetic.main.support_error_layout.* +import java.math.BigDecimal +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class LocalPaymentFragment : DaggerFragment(), LocalPaymentView { + + companion object { + + private const val DOMAIN_KEY = "domain" + private const val SKU_ID_KEY = "skuId" + private const val ORIGINAL_AMOUNT_KEY = "original_amount" + private const val CURRENCY_KEY = "currency" + private const val PAYMENT_KEY = "payment_name" + private const val BONUS_KEY = "bonus" + private const val STATUS_KEY = "status" + private const val ERROR_MESSAGE_KEY = "error_message" + private const val TYPE_KEY = "type" + private const val DEV_ADDRESS_KEY = "dev_address" + private const val AMOUNT_KEY = "amount" + private const val CALLBACK_URL = "CALLBACK_URL" + private const val ORDER_REFERENCE = "ORDER_REFERENCE" + private const val PAYLOAD = "PAYLOAD" + private const val PAYMENT_METHOD_URL = "payment_method_url" + private const val PAYMENT_METHOD_LABEL = "payment_method_label" + private const val GAMIFICATION_LEVEL = "gamification_level" + private const val ANIMATION_STEP_ONE_START_FRAME = 0 + private const val ANIMATION_STEP_TWO_START_FRAME = 80 + private const val MID_ANIMATION_FRAME_INCREMENT = 40 + private const val LAST_ANIMATION_FRAME_INCREMENT = 30 + private const val BUTTON_ANIMATION_START_FRAME = 120 + private const val LAST_ANIMATION_FRAME = 150 + + @JvmStatic + fun newInstance(domain: String, skudId: String?, originalAmount: String?, currency: String?, + bonus: String?, selectedPaymentMethod: String, developerAddress: String, + type: String, amount: BigDecimal, callbackUrl: String?, orderReference: String?, + payload: String?, paymentMethodIconUrl: String, + paymentMethodLabel: String, gamificationLevel: Int): LocalPaymentFragment { + return LocalPaymentFragment().apply { + arguments = Bundle().apply { + putString(DOMAIN_KEY, domain) + putString(SKU_ID_KEY, skudId) + putString(ORIGINAL_AMOUNT_KEY, originalAmount) + putString(CURRENCY_KEY, currency) + putString(BONUS_KEY, bonus) + putString(PAYMENT_KEY, selectedPaymentMethod) + putString(DEV_ADDRESS_KEY, developerAddress) + putString(TYPE_KEY, type) + putSerializable(AMOUNT_KEY, amount) + putString(CALLBACK_URL, callbackUrl) + putString(ORDER_REFERENCE, orderReference) + putString(PAYLOAD, payload) + putString(PAYMENT_METHOD_URL, paymentMethodIconUrl) + putString(PAYMENT_METHOD_LABEL, paymentMethodLabel) + putInt(GAMIFICATION_LEVEL, gamificationLevel) + } + } + } + } + + private val domain: String by lazy { + if (arguments!!.containsKey(DOMAIN_KEY)) { + arguments!!.getString(DOMAIN_KEY)!! + } else { + throw IllegalArgumentException("domain data not found") + } + } + + private val skudId: String? by lazy { + if (arguments!!.containsKey(SKU_ID_KEY)) { + arguments!!.getString(SKU_ID_KEY) + } else { + throw IllegalArgumentException("skuId data not found") + } + } + + private val originalAmount: String? by lazy { + if (arguments!!.containsKey(ORIGINAL_AMOUNT_KEY)) { + arguments!!.getString(ORIGINAL_AMOUNT_KEY) + } else { + throw IllegalArgumentException("original amount data not found") + } + } + + private val bonus: String? by lazy { + if (arguments!!.containsKey(BONUS_KEY)) { + arguments!!.getString(BONUS_KEY) + } else { + throw IllegalArgumentException("bonus amount data not found") + } + } + + private val paymentId: String by lazy { + if (arguments!!.containsKey(PAYMENT_KEY)) { + arguments!!.getString(PAYMENT_KEY)!! + } else { + throw IllegalArgumentException("payment method data not found") + } + } + + private val currency: String? by lazy { + if (arguments!!.containsKey(CURRENCY_KEY)) { + arguments!!.getString(CURRENCY_KEY) + } else { + throw IllegalArgumentException("currency data not found") + } + } + + private val developerAddress: String by lazy { + if (arguments!!.containsKey(DEV_ADDRESS_KEY)) { + arguments!!.getString(DEV_ADDRESS_KEY)!! + } else { + throw IllegalArgumentException("dev address data not found") + } + } + + private val type: String by lazy { + if (arguments!!.containsKey(TYPE_KEY)) { + arguments!!.getString(TYPE_KEY)!! + } else { + throw IllegalArgumentException("type data not found") + } + } + + private val amount: BigDecimal by lazy { + if (arguments!!.containsKey(AMOUNT_KEY)) { + arguments!!.getSerializable(AMOUNT_KEY) as BigDecimal + } else { + throw IllegalArgumentException("amount data not found") + } + } + + private val orderReference: String? by lazy { + if (arguments!!.containsKey(ORDER_REFERENCE)) { + arguments!!.getString(ORDER_REFERENCE) + } else { + throw IllegalArgumentException("dev address data not found") + } + } + + private val callbackUrl: String? by lazy { + if (arguments!!.containsKey(CALLBACK_URL)) { + arguments!!.getString(CALLBACK_URL) + } else { + throw IllegalArgumentException("dev address data not found") + } + } + + private val payload: String? by lazy { + if (arguments!!.containsKey(PAYLOAD)) { + arguments!!.getString(PAYLOAD) + } else { + throw IllegalArgumentException("dev address data not found") + } + } + + private val paymentMethodIconUrl: String? by lazy { + if (arguments!!.containsKey(PAYMENT_METHOD_URL)) { + arguments!!.getString(PAYMENT_METHOD_URL) + } else { + throw IllegalArgumentException("payment method icon url not found") + } + } + + private val paymentMethodIconLabel: String? by lazy { + if (arguments!!.containsKey(PAYMENT_METHOD_LABEL)) { + arguments!!.getString(PAYMENT_METHOD_LABEL) + } else { + throw IllegalArgumentException("payment method icon label not found") + } + } + + private val gamificationLevel: Int by lazy { + if (arguments!!.containsKey(GAMIFICATION_LEVEL)) { + arguments!!.getInt(GAMIFICATION_LEVEL) + } else { + throw IllegalArgumentException("gamification level not found") + } + } + + @Inject + lateinit var localPaymentInteractor: LocalPaymentInteractor + + @Inject + lateinit var analytics: LocalPaymentAnalytics + + @Inject + lateinit var logger: Logger + + private lateinit var iabView: IabView + private lateinit var navigator: FragmentNavigator + private lateinit var localPaymentPresenter: LocalPaymentPresenter + private lateinit var status: ViewState + private var errorMessage = R.string.activity_iab_error_message + private var minFrame = 0 + private var maxFrame = 40 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + navigator = FragmentNavigator(activity as UriNavigator?, iabView) + status = NONE + localPaymentPresenter = + LocalPaymentPresenter(this, originalAmount, currency, domain, skudId, + paymentId, developerAddress, localPaymentInteractor, navigator, type, amount, analytics, + savedInstanceState, AndroidSchedulers.mainThread(), Schedulers.io(), + CompositeDisposable(), callbackUrl, orderReference, payload, context, + paymentMethodIconUrl, gamificationLevel, logger) + } + + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putSerializable(STATUS_KEY, status) + outState.putInt(ERROR_MESSAGE_KEY, errorMessage) + localPaymentPresenter.onSaveInstanceState(outState) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check(context is IabView) { + throw IllegalStateException("Local payment fragment must be attached to IAB activity") + } + iabView = context + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + + localPaymentPresenter.present() + } + + private fun setupUi() { + if (bonus?.isNotBlank() == true) { + complete_payment_view.lottie_transaction_success.setAnimation( + R.raw.top_up_bonus_success_animation) + setAnimationText() + } else { + complete_payment_view.lottie_transaction_success.setAnimation(R.raw.top_up_success_animation) + } + + iabView.disableBack() + } + + override fun onViewStateRestored(savedInstanceState: Bundle?) { + if (savedInstanceState?.get(STATUS_KEY) != null) { + status = savedInstanceState.get(STATUS_KEY) as ViewState + errorMessage = savedInstanceState.getInt(ERROR_MESSAGE_KEY, errorMessage) + setViewState() + } + super.onViewStateRestored(savedInstanceState) + } + + private fun setViewState() = when (status) { + COMPLETED -> showCompletedPayment() + PENDING_USER_PAYMENT -> localPaymentPresenter.preparePendingUserPayment() + ERROR -> showError() + LOADING -> showProcessingLoading() + else -> Unit + } + + private fun setAnimationText() { + val textDelegate = TextDelegate(complete_payment_view.lottie_transaction_success) + textDelegate.setText("bonus_value", bonus) + textDelegate.setText("bonus_received", + resources.getString(R.string.gamification_purchase_completed_bonus_received)) + complete_payment_view.lottie_transaction_success.setTextDelegate(textDelegate) + complete_payment_view.lottie_transaction_success.setFontAssetDelegate(object : + FontAssetDelegate() { + override fun fetchFont(fontFamily: String?) = + Typeface.create("sans-serif-medium", Typeface.BOLD) + }) + } + + override fun onDestroyView() { + localPaymentPresenter.handleStop() + super.onDestroyView() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.local_payment_layout, container, false) + } + + override fun getErrorDismissClick() = RxView.clicks(error_view.error_dismiss) + + override fun getSupportLogoClicks() = RxView.clicks(layout_support_logo) + + override fun getSupportIconClicks() = RxView.clicks(layout_support_icn) + + override fun getGotItClick() = RxView.clicks(got_it_button) + + override fun showWalletValidation(error: Int) = iabView.showWalletValidation(error) + + override fun showProcessingLoading() { + status = LOADING + progress_bar.visibility = View.VISIBLE + error_view.visibility = View.GONE + pending_user_payment_view.visibility = View.GONE + pending_user_payment_view.in_progress_animation.cancelAnimation() + complete_payment_view.lottie_transaction_success.cancelAnimation() + } + + override fun hideLoading() { + progress_bar.visibility = View.GONE + error_view.visibility = View.GONE + pending_user_payment_view.in_progress_animation.cancelAnimation() + complete_payment_view.lottie_transaction_success.cancelAnimation() + pending_user_payment_view.visibility = View.GONE + complete_payment_view.visibility = View.GONE + } + + override fun showCompletedPayment() { + status = COMPLETED + progress_bar.visibility = View.GONE + error_view.visibility = View.GONE + pending_user_payment_view.visibility = View.GONE + complete_payment_view.visibility = View.VISIBLE + complete_payment_view.iab_activity_transaction_completed.visibility = View.VISIBLE + complete_payment_view.lottie_transaction_success.playAnimation() + pending_user_payment_view.in_progress_animation.cancelAnimation() + } + + override fun showPendingUserPayment(paymentMethodIcon: Bitmap, applicationIcon: Bitmap) { + status = PENDING_USER_PAYMENT + error_view.visibility = View.GONE + complete_payment_view.visibility = View.GONE + progress_bar.visibility = View.GONE + pending_user_payment_view?.visibility = View.VISIBLE + + val placeholder = getString(R.string.async_steps_1_no_notification) + val stepOneText = String.format(placeholder, paymentMethodIconLabel) + + step_one_desc.text = stepOneText + + pending_user_payment_view?.in_progress_animation?.updateBitmap("image_0", paymentMethodIcon) + pending_user_payment_view?.in_progress_animation?.updateBitmap("image_1", applicationIcon) + + playAnimation() + } + + override fun showError(message: Int?) { + status = ERROR + error_message.text = getString(R.string.ok) + message?.let { errorMessage = it } + error_message.text = getString(message ?: errorMessage) + pending_user_payment_view.visibility = View.GONE + complete_payment_view.visibility = View.GONE + pending_user_payment_view.in_progress_animation.cancelAnimation() + complete_payment_view.lottie_transaction_success.cancelAnimation() + progress_bar.visibility = View.GONE + error_view.visibility = View.VISIBLE + } + + override fun dismissError() { + status = NONE + error_view.visibility = View.GONE + iabView.finishWithError() + } + + override fun close() { + status = NONE + progress_bar.visibility = View.GONE + error_view.visibility = View.GONE + pending_user_payment_view.in_progress_animation.cancelAnimation() + complete_payment_view.lottie_transaction_success.cancelAnimation() + pending_user_payment_view.visibility = View.GONE + complete_payment_view.visibility = View.GONE + iabView.close(Bundle()) + } + + override fun getAnimationDuration() = complete_payment_view.lottie_transaction_success.duration + + override fun popView(bundle: Bundle) { + bundle.putString(InAppPurchaseInteractor.PRE_SELECTED_PAYMENT_METHOD_KEY, + paymentId) + iabView.finish(bundle) + } + + override fun lockRotation() { + iabView.lockRotation() + } + + private fun playAnimation() { + pending_user_payment_view?.in_progress_animation?.setMinAndMaxFrame(minFrame, maxFrame) + pending_user_payment_view?.in_progress_animation?.addAnimatorListener(object : + Animator.AnimatorListener { + override fun onAnimationRepeat(animation: Animator?) = Unit + + override fun onAnimationEnd(animation: Animator?) { + if (maxFrame == LAST_ANIMATION_FRAME) { + pending_user_payment_view?.in_progress_animation?.cancelAnimation() + } + if (minFrame == BUTTON_ANIMATION_START_FRAME) { + minFrame += LAST_ANIMATION_FRAME_INCREMENT + maxFrame += LAST_ANIMATION_FRAME_INCREMENT + pending_user_payment_view?.in_progress_animation?.setMinAndMaxFrame(minFrame, maxFrame) + pending_user_payment_view?.in_progress_animation?.playAnimation() + } else { + minFrame += MID_ANIMATION_FRAME_INCREMENT + maxFrame += MID_ANIMATION_FRAME_INCREMENT + pending_user_payment_view?.in_progress_animation?.setMinAndMaxFrame(minFrame, maxFrame) + pending_user_payment_view?.in_progress_animation?.playAnimation() + } + } + + override fun onAnimationCancel(animation: Animator?) = Unit + + override fun onAnimationStart(animation: Animator?) { + when (minFrame) { + ANIMATION_STEP_ONE_START_FRAME -> { + animateShow(step_one) + animateShow(step_one_desc) + } + ANIMATION_STEP_TWO_START_FRAME -> { + animateShow(step_two) + animateShow(step_two_desc) + } + BUTTON_ANIMATION_START_FRAME -> animateButton(got_it_button) + else -> return + } + } + }) + pending_user_payment_view?.in_progress_animation?.playAnimation() + } + + private fun animateShow(view: View) { + view.apply { + alpha = 0.0f + visibility = View.VISIBLE + lockRotation() + + animate() + .alpha(1f) + .withEndAction { this.visibility = View.VISIBLE } + .setDuration(TimeUnit.SECONDS.toMillis(1)) + .setListener(null) + } + } + + private fun animateButton(view: View) { + view.apply { + alpha = 0.2f + + animate() + .alpha(1f) + .withEndAction { + this.isClickable = true + iabView.enableBack() + } + .setDuration(TimeUnit.SECONDS.toMillis(1)) + .setListener(null) + } + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentInteractor.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentInteractor.kt new file mode 100644 index 00000000000..96a257ce6a4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentInteractor.kt @@ -0,0 +1,121 @@ +package com.asfoundation.wallet.ui.iab + +import android.net.Uri +import android.os.Bundle +import com.appcoins.wallet.bdsbilling.Billing +import com.appcoins.wallet.bdsbilling.WalletService +import com.appcoins.wallet.bdsbilling.repository.RemoteRepository +import com.appcoins.wallet.bdsbilling.repository.entity.Transaction +import com.appcoins.wallet.bdsbilling.repository.entity.Transaction.Status.* +import com.appcoins.wallet.billing.BillingMessagesMapper +import com.asfoundation.wallet.billing.partners.AddressService +import com.asfoundation.wallet.billing.purchase.InAppDeepLinkRepository +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.functions.BiFunction +import java.util.* + +class LocalPaymentInteractor(private val deepLinkRepository: InAppDeepLinkRepository, + private val walletService: WalletService, + private val partnerAddressService: AddressService, + private val inAppPurchaseInteractor: InAppPurchaseInteractor, + private val billing: Billing, + private val billingMessagesMapper: BillingMessagesMapper, + private val supportInteractor: SupportInteractor, + private val walletBlockedInteract: WalletBlockedInteract, + private val smsValidationInteract: SmsValidationInteract, + private val remoteRepository: RemoteRepository) { + + fun isWalletBlocked() = walletBlockedInteract.isWalletBlocked() + + fun isWalletVerified() = + walletService.getWalletAddress() + .flatMap { smsValidationInteract.isValidated(it) } + .onErrorReturn { true } + + fun getPaymentLink(domain: String, skuId: String?, originalAmount: String?, + originalCurrency: String?, paymentMethod: String, developerAddress: String, + callbackUrl: String?, orderReference: String?, + payload: String?): Single { + + return walletService.getAndSignCurrentWalletAddress() + .flatMap { walletAddressModel -> + Single.zip( + partnerAddressService.getStoreAddressForPackage(domain), + partnerAddressService.getOemAddressForPackage(domain), + BiFunction { storeAddress: String, oemAddress: String -> + DeepLinkInformation(storeAddress, oemAddress) + }) + .flatMap { + deepLinkRepository.getDeepLink(domain, skuId, walletAddressModel.address, + walletAddressModel.signedAddress, originalAmount, originalCurrency, + paymentMethod, developerAddress, it.storeAddress, it.oemAddress, callbackUrl, + orderReference, payload) + } + } + } + + fun getTopUpPaymentLink(packageName: String, fiatAmount: String, + fiatCurrency: String, paymentMethod: String, + productName: String): Single { + + return walletService.getAndSignCurrentWalletAddress() + .flatMap { walletAddressModel -> + remoteRepository.createLocalPaymentTopUpTransaction(paymentMethod, packageName, + fiatAmount, fiatCurrency, productName, walletAddressModel.address, + walletAddressModel.signedAddress) + } + .map { it.url ?: "" } + } + + fun getTransaction(uri: Uri): Observable = + inAppPurchaseInteractor.getTransaction(uri.lastPathSegment) + .filter { isEndingState(it.status, it.type) } + .distinctUntilChanged { transaction -> transaction.status } + + private fun isEndingState(status: Transaction.Status, type: String) = + (status == PENDING_USER_PAYMENT && type == "TOPUP") || + status == COMPLETED || + status == FAILED || + status == CANCELED || + status == INVALID_TRANSACTION + + fun getCompletePurchaseBundle(type: String, merchantName: String, sku: String?, + orderReference: String?, hash: String?, + scheduler: Scheduler): Single = + if (isInApp(type) && sku != null) { + billing.getSkuPurchase(merchantName, sku, scheduler) + .map { billingMessagesMapper.mapPurchase(it, orderReference) } + } else { + Single.just(billingMessagesMapper.successBundle(hash)) + } + + private fun isInApp(type: String) = type.equals("INAPP", ignoreCase = true) + + fun savePreSelectedPaymentMethod(paymentMethod: String) { + inAppPurchaseInteractor.savePreSelectedPaymentMethod(paymentMethod) + } + + fun saveAsyncLocalPayment(paymentMethod: String) { + inAppPurchaseInteractor.saveAsyncLocalPayment(paymentMethod) + } + + fun showSupport(gamificationLevel: Int): Completable { + return walletService.getWalletAddress() + .flatMapCompletable { + Completable.fromAction { + supportInteractor.registerUser(gamificationLevel, it.toLowerCase(Locale.ROOT)) + supportInteractor.displayChatScreen() + } + } + } + + private data class DeepLinkInformation(val storeAddress: String, val oemAddress: String) + + fun isAsync(type: String) = type == "TOPUP" +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentPresenter.kt new file mode 100644 index 00000000000..61b5caa2bb0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentPresenter.kt @@ -0,0 +1,295 @@ +package com.asfoundation.wallet.ui.iab + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.os.Bundle +import android.util.TypedValue +import androidx.annotation.StringRes +import com.appcoins.wallet.bdsbilling.repository.entity.Transaction +import com.appcoins.wallet.bdsbilling.repository.entity.Transaction.Status +import com.asf.wallet.R +import com.asfoundation.wallet.GlideApp +import com.asfoundation.wallet.logging.Logger +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.BiFunction +import retrofit2.HttpException +import java.math.BigDecimal +import java.util.concurrent.TimeUnit + + +class LocalPaymentPresenter(private val view: LocalPaymentView, + private val originalAmount: String?, + private val currency: String?, + private val domain: String, + private val skuId: String?, + private val paymentId: String, + private val developerAddress: String, + private val localPaymentInteractor: LocalPaymentInteractor, + private val navigator: FragmentNavigator, + private val type: String, + private val amount: BigDecimal, + private val analytics: LocalPaymentAnalytics, + private val savedInstance: Bundle?, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val callbackUrl: String?, + private val orderReference: String?, + private val payload: String?, + private val context: Context?, + private val paymentMethodIconUrl: String?, + private val gamificationLevel: Int, + private val logger: Logger) { + + private var waitingResult: Boolean = false + + fun present() { + if (savedInstance != null) { + waitingResult = savedInstance.getBoolean(WAITING_RESULT) + } + onViewCreatedRequestLink() + handlePaymentRedirect() + handleOkErrorButtonClick() + handleOkBuyButtonClick() + handleSupportClicks() + } + + fun handleStop() { + waitingResult = false + disposables.clear() + } + + fun preparePendingUserPayment() { + disposables.add( + Single.zip( + getPaymentMethodIcon(), + getApplicationIcon(), + BiFunction { paymentMethodIcon: Bitmap, applicationIcon: Bitmap -> + Pair(paymentMethodIcon, applicationIcon) + } + ) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .subscribe({ view.showPendingUserPayment(it.first, it.second) }, { showError(it) })) + } + + private fun getPaymentMethodIcon() = Single.fromCallable { + GlideApp.with(context!!) + .asBitmap() + .load(paymentMethodIconUrl) + .override(getWidth(), getHeight()) + .centerCrop() + .submit() + .get() + } + + private fun getApplicationIcon() = Single.fromCallable { + val applicationIcon = + (context!!.packageManager.getApplicationIcon(domain) as BitmapDrawable).bitmap + + Bitmap.createScaledBitmap(applicationIcon, appIconWidth, appIconHeight, true) + } + + private fun onViewCreatedRequestLink() { + disposables.add( + localPaymentInteractor.getPaymentLink(domain, skuId, originalAmount, currency, paymentId, + developerAddress, callbackUrl, orderReference, payload) + .filter { !waitingResult } + .observeOn(viewScheduler) + .doOnSuccess { + analytics.sendPaymentMethodDetailsEvent(domain, skuId, amount.toString(), type, + paymentId) + analytics.sendPaymentConfirmationEvent(domain, skuId, amount.toString(), type, + paymentId) + navigator.navigateToUriForResult(it) + waitingResult = true + } + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .subscribe({ }, { showError(it) })) + } + + private fun handlePaymentRedirect() { + disposables.add(navigator.uriResults() + .doOnNext { view.showProcessingLoading() } + .doOnNext { view.lockRotation() } + .flatMap { + localPaymentInteractor.getTransaction(it) + .subscribeOn(networkScheduler) + } + .observeOn(viewScheduler) + .flatMapCompletable { handleTransactionStatus(it) } + .subscribe({}, { showError(it) })) + } + + private fun handleOkErrorButtonClick() { + disposables.add(view.getErrorDismissClick() + .doOnNext { view.dismissError() } + .subscribe({}, { view.dismissError() })) + } + + private fun handleOkBuyButtonClick() { + disposables.add(view.getGotItClick() + .doOnNext { view.close() } + .subscribe({}, { view.close() }) + ) + } + + private fun handleFraudFlow() { + disposables.add( + localPaymentInteractor.isWalletBlocked() + .subscribeOn(networkScheduler) + .observeOn(networkScheduler) + .flatMap { blocked -> + if (blocked) { + localPaymentInteractor.isWalletVerified() + .observeOn(viewScheduler) + .doOnSuccess { + if (it) view.showError(R.string.purchase_error_wallet_block_code_403) + else view.showWalletValidation(R.string.purchase_error_wallet_block_code_403) + } + } else { + Single.just(true) + .observeOn(viewScheduler) + .doOnSuccess { view.showError(R.string.purchase_error_wallet_block_code_403) } + } + } + .observeOn(viewScheduler) + .subscribe({}, { + logger.log(TAG, it) + view.showError(R.string.purchase_error_wallet_block_code_403) + }) + ) + } + + private fun handleTransactionStatus(transaction: Transaction): Completable { + view.hideLoading() + return when { + isErrorStatus(transaction) -> Completable.fromAction { + logger.log(TAG, "Transaction came with error status: ${transaction.status}") + view.showError() + } + .subscribeOn(viewScheduler) + localPaymentInteractor.isAsync(transaction.type) -> + handleAsyncTransactionStatus(transaction) + .andThen(Completable.fromAction { + localPaymentInteractor.savePreSelectedPaymentMethod(paymentId) + localPaymentInteractor.saveAsyncLocalPayment(paymentId) + preparePendingUserPayment() + }) + transaction.status == Status.COMPLETED -> handleSyncCompletedStatus(transaction) + else -> Completable.complete() + } + } + + private fun isErrorStatus(transaction: Transaction) = + transaction.status == Status.FAILED || + transaction.status == Status.CANCELED || + transaction.status == Status.INVALID_TRANSACTION + + private fun handleSyncCompletedStatus(transaction: Transaction): Completable { + return localPaymentInteractor.getCompletePurchaseBundle(type, domain, skuId, + transaction.orderReference, transaction.hash, networkScheduler) + .doOnSuccess { + analytics.sendPaymentEvent(domain, skuId, amount.toString(), type, paymentId) + analytics.sendPaymentConclusionEvent(domain, skuId, amount.toString(), type, + paymentId) + analytics.sendRevenueEvent(disposables, amount) + } + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .flatMapCompletable { + Completable.fromAction { view.showCompletedPayment() } + .andThen(Completable.timer(view.getAnimationDuration(), TimeUnit.MILLISECONDS)) + .andThen(Completable.fromAction { view.popView(it) }) + } + } + + private fun handleAsyncTransactionStatus(transaction: Transaction): Completable { + return when (transaction.status) { + Status.PENDING_USER_PAYMENT -> { + Completable.fromAction { + analytics.sendPaymentEvent(domain, skuId, amount.toString(), type, paymentId) + analytics.sendPaymentPendingEvent(domain, skuId, amount.toString(), type, paymentId) + } + } + Status.COMPLETED -> { + Completable.fromAction { + analytics.sendPaymentEvent(domain, skuId, amount.toString(), type, paymentId) + analytics.sendPaymentConclusionEvent(domain, skuId, amount.toString(), type, + paymentId) + analytics.sendRevenueEvent(disposables, amount) + } + } + else -> Completable.complete() + } + } + + private fun handleSupportClicks() { + disposables.add(Observable.merge(view.getSupportIconClicks(), view.getSupportLogoClicks()) + .throttleFirst(50, TimeUnit.MILLISECONDS) + .flatMapCompletable { localPaymentInteractor.showSupport(gamificationLevel) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun showError(throwable: Throwable) { + logger.log(TAG, throwable) + if (throwable is HttpException && throwable.code() == FORBIDDEN_CODE) handleFraudFlow() + else view.showError(mapError(throwable)) + } + + fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(WAITING_RESULT, waitingResult) + } + + private fun getWidth(): Int { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 184f, + context?.resources?.displayMetrics) + .toInt() + } + + private fun getHeight(): Int { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 80f, + context?.resources?.displayMetrics) + .toInt() + } + + private val appIconWidth: Int + get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 160f, + context?.resources?.displayMetrics) + .toInt() + + private val appIconHeight: Int + get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 160f, + context?.resources?.displayMetrics) + .toInt() + + companion object { + private val TAG = LocalPaymentPresenter::class.java.simpleName + private const val WAITING_RESULT = "WAITING_RESULT" + private const val FORBIDDEN_CODE = 403 + } + + @StringRes + private fun mapError(throwable: Throwable): Int { + return when (throwable) { + is HttpException -> mapHttpError(throwable) + else -> R.string.unknown_error + } + } + + @StringRes + private fun mapHttpError(exceptiont: HttpException): Int { + return when (exceptiont.code()) { + FORBIDDEN_CODE -> R.string.purchase_error_wallet_block_code_403 + else -> R.string.unknown_error + } + } +} + diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentView.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentView.kt new file mode 100644 index 00000000000..ba98ea8eda0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/LocalPaymentView.kt @@ -0,0 +1,43 @@ +package com.asfoundation.wallet.ui.iab + +import android.graphics.Bitmap +import android.os.Bundle +import androidx.annotation.StringRes +import io.reactivex.Observable + +interface LocalPaymentView { + + fun showProcessingLoading() + + fun hideLoading() + + fun showPendingUserPayment(paymentMethodIcon: Bitmap, applicationIcon: Bitmap) + + fun showCompletedPayment() + + fun showError(message: Int? = null) + + fun dismissError() + + fun getErrorDismissClick(): Observable + + fun getGotItClick(): Observable + + fun close() + + fun getAnimationDuration(): Long + + fun popView(bundle: Bundle) + + fun lockRotation() + + fun getSupportLogoClicks(): Observable + + fun getSupportIconClicks(): Observable + + fun showWalletValidation(@StringRes error: Int) + + enum class ViewState { + NONE, COMPLETED, PENDING_USER_PAYMENT, ERROR, LOADING + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsBalance.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsBalance.kt new file mode 100644 index 00000000000..e3dbcf5c8d8 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsBalance.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.ui.iab + +import java.math.BigDecimal + +data class MergedAppcoinsBalance(val appcFiatValue: FiatValue, val creditsFiatBalance: FiatValue, + val creditsAppcAmount: BigDecimal) + diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsFragment.kt new file mode 100644 index 00000000000..ce090138a87 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsFragment.kt @@ -0,0 +1,522 @@ +package com.asfoundation.wallet.ui.iab + +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Typeface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.* +import android.view.ViewGroup +import android.view.animation.AnimationUtils +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.appcompat.widget.AppCompatRadioButton +import androidx.constraintlayout.widget.Group +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.navigator.UriNavigator +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.appcoins_radio_button.* +import kotlinx.android.synthetic.main.credits_radio_button.* +import kotlinx.android.synthetic.main.credits_radio_button.view.* +import kotlinx.android.synthetic.main.dialog_buy_app_info_header.app_icon +import kotlinx.android.synthetic.main.dialog_buy_app_info_header.app_name +import kotlinx.android.synthetic.main.dialog_buy_app_info_header.app_sku_description +import kotlinx.android.synthetic.main.dialog_buy_buttons.* +import kotlinx.android.synthetic.main.iab_error_layout.* +import kotlinx.android.synthetic.main.merged_appcoins_layout.* +import kotlinx.android.synthetic.main.payment_methods_header.* +import kotlinx.android.synthetic.main.support_error_layout.* +import kotlinx.android.synthetic.main.view_purchase_bonus.* +import kotlinx.android.synthetic.main.view_purchase_bonus.view.* +import java.math.BigDecimal +import javax.inject.Inject + +class MergedAppcoinsFragment : DaggerFragment(), MergedAppcoinsView { + + companion object { + private const val FIAT_AMOUNT_KEY = "fiat_amount" + private const val FIAT_CURRENCY_KEY = "currency_amount" + private const val BONUS_KEY = "bonus" + private const val APP_NAME_KEY = "app_name" + private const val PRODUCT_NAME_KEY = "product_name" + private const val APPC_AMOUNT_KEY = "appc_amount" + private const val IS_BDS_KEY = "is_bds" + private const val IS_DONATION_KEY = "is_donation" + private const val SKU_ID = "sku_id" + private const val TRANSACTION_TYPE = "transaction_type" + private const val GAMIFICATION_LEVEL = "gamification_level" + private const val TRANSACTION_BUILDER = "transaction_builder" + const val APPC = "appcoins" + const val CREDITS = "credits" + + @JvmStatic + fun newInstance(fiatAmount: BigDecimal, currency: String, bonus: String, appName: String, + productName: String?, appcAmount: BigDecimal, isBds: Boolean, + isDonation: Boolean, skuId: String?, + transactionType: String, gamificationLevel: Int, + transactionBuilder: TransactionBuilder): Fragment { + val fragment = MergedAppcoinsFragment() + val bundle = Bundle().apply { + putSerializable(FIAT_AMOUNT_KEY, fiatAmount) + putString(FIAT_CURRENCY_KEY, currency) + putString(BONUS_KEY, bonus) + putString(APP_NAME_KEY, appName) + putString(PRODUCT_NAME_KEY, productName) + putSerializable(APPC_AMOUNT_KEY, appcAmount) + putBoolean(IS_BDS_KEY, isBds) + putBoolean(IS_DONATION_KEY, isDonation) + putString(SKU_ID, skuId) + putString(TRANSACTION_TYPE, transactionType) + putInt(GAMIFICATION_LEVEL, gamificationLevel) + putParcelable(TRANSACTION_BUILDER, transactionBuilder) + } + fragment.arguments = bundle + return fragment + } + } + + private lateinit var mergedAppcoinsPresenter: MergedAppcoinsPresenter + private var paymentSelectionSubject: PublishSubject? = null + private lateinit var iabView: IabView + + @Inject + lateinit var billingAnalytics: BillingAnalytics + + @Inject + lateinit var formatter: CurrencyFormatUtils + + @Inject + lateinit var mergedAppcoinsInteractor: MergedAppcoinsInteractor + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var paymentMethodsMapper: PaymentMethodsMapper + + private val fiatAmount: BigDecimal by lazy { + if (arguments!!.containsKey(FIAT_AMOUNT_KEY)) { + arguments!!.getSerializable(FIAT_AMOUNT_KEY) as BigDecimal + } else { + throw IllegalArgumentException("amount data not found") + } + } + private val currency: String by lazy { + if (arguments!!.containsKey(FIAT_CURRENCY_KEY)) { + arguments!!.getString(FIAT_CURRENCY_KEY)!! + } else { + throw IllegalArgumentException("currency data not found") + } + } + + private val bonus: String by lazy { + if (arguments!!.containsKey(BONUS_KEY)) { + arguments!!.getString(BONUS_KEY)!! + } else { + throw IllegalArgumentException("bonus data not found") + } + } + + private val appName: String by lazy { + if (arguments!!.containsKey(APP_NAME_KEY)) { + arguments!!.getString(APP_NAME_KEY)!! + } else { + throw IllegalArgumentException("app name data not found") + } + } + + private val productName: String? by lazy { + if (arguments!!.containsKey(PRODUCT_NAME_KEY)) { + arguments!!.getString(PRODUCT_NAME_KEY) + } else { + throw IllegalArgumentException("product name data not found") + } + } + + private val appcAmount: BigDecimal by lazy { + if (arguments!!.containsKey(APPC_AMOUNT_KEY)) { + arguments!!.getSerializable(APPC_AMOUNT_KEY) as BigDecimal + } else { + throw IllegalArgumentException("appc data not found") + } + } + + private val isBds: Boolean by lazy { + if (arguments!!.containsKey(IS_BDS_KEY)) { + arguments!!.getBoolean(IS_BDS_KEY) + } else { + throw IllegalArgumentException("is bds data not found") + } + } + + private val isDonation: Boolean by lazy { + if (arguments!!.containsKey(IS_DONATION_KEY)) { + arguments!!.getBoolean(IS_DONATION_KEY) + } else { + throw IllegalArgumentException("is donation data not found") + } + } + + private val skuId: String? by lazy { + arguments!!.getString(SKU_ID) + } + + private val transactionType: String by lazy { + if (arguments!!.containsKey(TRANSACTION_TYPE)) { + arguments!!.getString(TRANSACTION_TYPE)!! + } else { + throw IllegalArgumentException("transaction type data not found") + } + } + + private val gamificationLevel: Int by lazy { + if (arguments!!.containsKey(GAMIFICATION_LEVEL)) { + arguments!!.getInt(GAMIFICATION_LEVEL) + } else { + throw IllegalArgumentException("gamification level not found") + } + } + + private val transactionBuilder: TransactionBuilder by lazy { + if (arguments!!.containsKey(TRANSACTION_BUILDER)) { + (arguments!!.getParcelable(TRANSACTION_BUILDER) as TransactionBuilder?)!! + } else { + throw IllegalArgumentException("transaction builder not found") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val navigator = FragmentNavigator(activity as UriNavigator?, iabView) + paymentSelectionSubject = PublishSubject.create() + mergedAppcoinsPresenter = + MergedAppcoinsPresenter(this, CompositeDisposable(), CompositeDisposable(), + AndroidSchedulers.mainThread(), Schedulers.io(), billingAnalytics, + formatter, mergedAppcoinsInteractor, gamificationLevel, navigator, logger, + transactionBuilder, paymentMethodsMapper) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + check(context is IabView) { "Merged Appcoins fragment must be attached to IAB activity" } + iabView = context + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.merged_appcoins_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setHeaderInformation() + buy_button.text = setBuyButtonText() + cancel_button.text = getString(R.string.back_button) + setBonus() + iabView.disableBack() + mergedAppcoinsPresenter.present(savedInstanceState) + } + + override fun showLoading() { + payment_methods.visibility = INVISIBLE + loading_view?.visibility = VISIBLE + } + + override fun hideLoading() { + loading_view?.visibility = GONE + payment_methods.visibility = VISIBLE + } + + private fun setBuyButtonText(): String { + return if (isDonation) getString(R.string.action_donate) else getString(R.string.action_buy) + } + + private fun setBonus() { + if (bonus.isNotEmpty()) { + //Build string for both landscape (header) and portrait (radio button) bonus layout + appcoins_radio?.bonus_value?.text = + getString(R.string.gamification_purchase_header_part_2, bonus) + bonus_value?.text = getString(R.string.gamification_purchase_header_part_2, bonus) + + //Set visibility for both landscape (header) and portrait (radio button) bonus layout + if (appcoins_radio_button.isChecked) { + bonus_layout?.visibility = VISIBLE + bonus_msg?.visibility = VISIBLE + } + appcoins_bonus_layout?.visibility = VISIBLE + } else { + appcoins_bonus_layout?.visibility = GONE + } + } + + private fun setHeaderInformation() { + if (isDonation) { + app_name.text = getString(R.string.item_donation) + app_sku_description.text = getString(R.string.item_donation) + } else { + app_name.text = getApplicationName(appName) + app_sku_description.text = productName + } + try { + app_icon.setImageDrawable(context!!.packageManager + .getApplicationIcon(appName)) + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + } + val appcText = formatter.formatCurrency(appcAmount, WalletCurrency.APPCOINS) + .plus(" " + WalletCurrency.APPCOINS.symbol) + val fiatText = formatter.formatCurrency(fiatAmount, WalletCurrency.FIAT) + .plus(" $currency") + fiat_price.text = fiatText + appc_price.text = appcText + fiat_price_skeleton.visibility = GONE + appc_price_skeleton.visibility = GONE + fiat_price.visibility = VISIBLE + appc_price.visibility = VISIBLE + } + + override fun setPaymentsInformation(hasCredits: Boolean, creditsDisableReason: Int?, + hasAppc: Boolean, appcDisabledReason: Int?) { + + if (hasAppc) { + setEnabledRadio(appcoins_radio, appcoins_radio_button, credits_radio_button, + appcoins_radio.title, appcoins_radio.message, appcoins_radio.icon, appcoins_bonus_layout, + appc_balances_group, APPC) + } else { + setDisabledRadio(appcoins_radio, appcoins_radio_button, appcoins_radio.title, + appcoins_radio.message, appcoins_radio.icon, appcoins_bonus_layout, appc_balances_group, + appcDisabledReason, R.string.purchase_appcoins_noavailable_body) + } + if (hasCredits) { + setEnabledRadio(credits_radio, credits_radio_button, appcoins_radio_button, + credits_radio.title, credits_radio.message, credits_radio.icon, null, + credits_balances_group, CREDITS) + credits_radio_button.isChecked = true + } else { + setDisabledRadio(credits_radio, credits_radio_button, credits_radio.title, + credits_radio.message, credits_radio.icon, null, credits_balances_group, + creditsDisableReason, R.string.purchase_appcoins_credits_noavailable_body) + appcoins_radio_button.isChecked = hasAppc + } + if (hasAppc || hasCredits) buy_button.isEnabled = true + else { + bonus_layout?.visibility = INVISIBLE + bonus_msg?.visibility = INVISIBLE + } + } + + private fun setDisabledRadio(view: View, radioButton: AppCompatRadioButton, title: TextView, + message: TextView, icon: ImageView, bonusLayout: View?, + balanceGroup: Group, disabledReason: Int?, + defaultDisabledReason: Int) { + view.setOnClickListener(null) + radioButton.setOnCheckedChangeListener(null) + val reason = disabledReason ?: defaultDisabledReason + radioButton.isEnabled = false + radioButton.isChecked = false + message.text = getString(reason) + title.setTextColor(ContextCompat.getColor(context!!, R.color.btn_disable_snd_color)) + message.setTextColor(ContextCompat.getColor(context!!, R.color.disable_reason)) + bonusLayout?.setBackgroundResource(R.drawable.disable_bonus_img_background) + message.visibility = VISIBLE + balanceGroup.visibility = INVISIBLE + balanceGroup.requestLayout() + title.typeface = Typeface.create("sans-serif", Typeface.NORMAL) + applyAlphaScale(icon) + } + + private fun setEnabledRadio(view: View, selectedRadioButton: AppCompatRadioButton, + unSelectedRadioButton: AppCompatRadioButton, title: TextView, + message: TextView, icon: ImageView, bonusView: View?, + balanceGroup: Group, method: String) { + view.setOnClickListener { selectedRadioButton.isChecked = true } + selectedRadioButton.setOnCheckedChangeListener { _, checked -> + if (checked) paymentSelectionSubject?.onNext(method) + setTitle(checked, title) + unSelectedRadioButton.isChecked = !checked + } + selectedRadioButton.isEnabled = true + message.text = "" + title.setTextColor(ContextCompat.getColor(context!!, R.color.color_title)) + bonusView?.setBackgroundResource(R.drawable.bonus_img_background) + message.visibility = INVISIBLE + balanceGroup.visibility = VISIBLE + balanceGroup.requestLayout() + icon.colorFilter = null + } + + private fun setTitle(checked: Boolean, title: TextView) { + if (checked) { + title.setTextColor( + ContextCompat.getColor(requireContext(), R.color.details_address_text_color)) + title.typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL) + } else { + title.setTextColor( + ContextCompat.getColor(requireContext(), R.color.grey_alpha_active_54)) + title.typeface = Typeface.create("sans-serif", Typeface.NORMAL) + } + } + + private fun applyAlphaScale(imageView: ImageView) { + val colorMatrix = ColorMatrix() + colorMatrix.setSaturation(0f) + val filter = ColorMatrixColorFilter(colorMatrix) + imageView.colorFilter = filter + } + + private fun getApplicationName(appPackage: String): CharSequence { + val packageManager = context!!.packageManager + val packageInfo = packageManager.getApplicationInfo(appPackage, 0) + return packageManager.getApplicationLabel(packageInfo) + } + + private fun getSelectedPaymentMethod(): String { + var selectedPaymentMethod = "" + if (appcoins_radio_button.isChecked) selectedPaymentMethod = APPC + if (credits_radio_button.isChecked) selectedPaymentMethod = CREDITS + return selectedPaymentMethod + } + + override fun buyClick(): Observable { + return RxView.clicks(buy_button) + .map { + PaymentInfoWrapper(appName, skuId, appcAmount.toString(), getSelectedPaymentMethod(), + transactionType) + } + } + + override fun backClick(): Observable { + return RxView.clicks(cancel_button) + .map { + PaymentInfoWrapper(appName, skuId, appcAmount.toString(), getSelectedPaymentMethod(), + transactionType) + } + } + + override fun backPressed(): Observable { + return iabView.backButtonPress() + .map { + PaymentInfoWrapper(appName, skuId, appcAmount.toString(), getSelectedPaymentMethod(), + transactionType) + } + } + + + override fun getPaymentSelection(): Observable { + return paymentSelectionSubject!! + } + + override fun hideBonus() { + bonus_layout?.visibility = INVISIBLE + bonus_msg?.visibility = INVISIBLE + } + + override fun showBonus() { + if (bonus.isNotEmpty()) { + val animation = AnimationUtils.loadAnimation(context, R.anim.fade_in_animation) + animation.duration = 250 + bonus_layout?.visibility = VISIBLE + bonus_layout?.startAnimation(animation) + bonus_msg?.visibility = VISIBLE + } else { + bonus_layout?.visibility = GONE + bonus_msg?.visibility = GONE + } + } + + override fun showError(@StringRes errorMessage: Int) { + payment_method_main_view.visibility = GONE + error_dismiss.text = getString(R.string.ok) + error_message.text = getString(errorMessage) + merged_error_layout.visibility = VISIBLE + } + + override fun errorDismisses() = RxView.clicks(error_dismiss) + + override fun getSupportLogoClicks() = RxView.clicks(layout_support_logo) + + override fun getSupportIconClicks() = RxView.clicks(layout_support_icn) + + override fun showAuthenticationActivity() { + iabView.showAuthenticationActivity() + } + + override fun navigateToAppcPayment() = + iabView.showOnChain(fiatAmount, isBds, bonus, gamificationLevel) + + override fun navigateToCreditsPayment() = + iabView.showAppcoinsCreditsPayment(appcAmount, gamificationLevel) + + override fun updateBalanceValues(appcFiat: String, creditsFiat: String, currency: String) { + balance_fiat_appc_eth.text = + getString(R.string.purchase_current_balance_appc_eth_body, "$appcFiat $currency") + credits_fiat_balance.text = + getString(R.string.purchase_current_balance_appcc_body, "$creditsFiat $currency") + } + + override fun toggleSkeletons(show: Boolean) { + if (show) { + skeleton_appcoins.visibility = VISIBLE + skeleton_credits.visibility = VISIBLE + payment_methods_group.visibility = INVISIBLE + } else { + skeleton_appcoins.visibility = GONE + skeleton_credits.visibility = GONE + payment_methods_group.visibility = VISIBLE + } + } + + override fun onAuthenticationResult(): Observable { + return iabView.onAuthenticationResult() + } + + override fun showPaymentMethodsView() = iabView.showPaymentMethodsView() + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + mergedAppcoinsPresenter.onSavedInstanceState(outState) + } + + override fun onResume() { + super.onResume() + buy_button.isEnabled = false + mergedAppcoinsPresenter.onResume() + } + + override fun onPause() { + mergedAppcoinsPresenter.handlePause() + super.onPause() + } + + override fun onDestroyView() { + mergedAppcoinsPresenter.handleStop() + iabView.enableBack() + appcoins_radio_button.setOnCheckedChangeListener(null) + appcoins_radio.setOnClickListener(null) + credits_radio_button.setOnCheckedChangeListener(null) + credits_radio.setOnClickListener(null) + super.onDestroyView() + } + + override fun onDestroy() { + paymentSelectionSubject = null + super.onDestroy() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsInteractor.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsInteractor.kt new file mode 100644 index 00000000000..95f8a87672c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsInteractor.kt @@ -0,0 +1,59 @@ +package com.asfoundation.wallet.ui.iab + +import android.util.Pair +import com.appcoins.wallet.bdsbilling.WalletService +import com.asf.wallet.R +import com.asfoundation.wallet.entity.Balance +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.interact.GetDefaultWalletBalanceInteract.BalanceState +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.ui.balance.BalanceInteract +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +import java.util.* + +class MergedAppcoinsInteractor(private val balanceInteract: BalanceInteract, + private val walletBlockedInteract: WalletBlockedInteract, + private val supportInteractor: SupportInteractor, + private val inAppPurchaseInteractor: InAppPurchaseInteractor, + private val walletService: WalletService, + private val preferencesRepositoryType: PreferencesRepositoryType) { + + fun showSupport(gamificationLevel: Int): Completable { + return walletService.getWalletAddress() + .flatMapCompletable { + Completable.fromAction { + supportInteractor.registerUser(gamificationLevel, it.toLowerCase(Locale.ROOT)) + supportInteractor.displayChatScreen() + } + } + } + + fun getEthBalance(): Observable = balanceInteract.getEthBalance() + .map { it.second } + + fun getAppcBalance(): Observable = balanceInteract.getAppcBalance() + .map { it.second } + + fun getCreditsBalance(): Observable> = + balanceInteract.getCreditsBalance() + + fun isWalletBlocked() = walletBlockedInteract.isWalletBlocked() + + fun hasAppcFunds(transactionBuilder: TransactionBuilder): Single { + return inAppPurchaseInteractor.getBalanceState(transactionBuilder) + .map { + when (it) { + BalanceState.NO_ETHER -> Availability(false, R.string.purchase_no_eth_body) + BalanceState.NO_TOKEN, BalanceState.NO_ETHER_NO_TOKEN -> Availability(false, + R.string.purchase_no_appcoins_body) + BalanceState.OK -> Availability(true, null) + } + } + } + + fun hasAuthenticationPermission() = preferencesRepositoryType.hasAuthenticationPermission() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsPresenter.kt new file mode 100644 index 00000000000..b6df65614a0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsPresenter.kt @@ -0,0 +1,238 @@ +package com.asfoundation.wallet.ui.iab + +import android.os.Bundle +import android.util.Log +import android.util.Pair +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.entity.Balance +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.ui.iab.MergedAppcoinsFragment.Companion.APPC +import com.asfoundation.wallet.ui.iab.MergedAppcoinsFragment.Companion.CREDITS +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.asfoundation.wallet.util.isNoNetworkException +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.BiFunction +import io.reactivex.functions.Function3 +import java.math.BigDecimal +import java.util.concurrent.TimeUnit + +class MergedAppcoinsPresenter(private val view: MergedAppcoinsView, + private val disposables: CompositeDisposable, + private val resumeDisposables: CompositeDisposable, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val analytics: BillingAnalytics, + private val formatter: CurrencyFormatUtils, + private val mergedAppcoinsInteractor: MergedAppcoinsInteractor, + private val gamificationLevel: Int, + private val navigator: Navigator, + private val logger: Logger, + private val transactionBuilder: TransactionBuilder, + private val paymentMethodsMapper: PaymentMethodsMapper) { + + private var cachedSelectedPaymentId: String? = null + + companion object { + private val TAG = MergedAppcoinsFragment::class.java.simpleName + private const val SELECTED_PAYMENT_ID = "selected_paymentId" + } + + fun present(savedInstanceState: Bundle?) { + savedInstanceState?.let { cachedSelectedPaymentId = it.getString(SELECTED_PAYMENT_ID) } + handlePaymentSelectionChange() + handleBuyClick() + handleBackClick() + handleSupportClicks() + handleErrorDismiss() + handleAuthenticationResult() + } + + fun onResume() { + fetchBalance() + } + + fun handleStop() = disposables.clear() + + fun handlePause() = resumeDisposables.clear() + + private fun fetchBalance() { + resumeDisposables.add(Observable.zip(getAppcBalance(), getCreditsBalance(), getEthBalance(), + Function3 { appcBalance: FiatValue, creditsBalance: Pair, ethBalance: FiatValue -> + val appcFiatValue = + FiatValue(appcBalance.amount.plus(ethBalance.amount), appcBalance.currency, + appcBalance.symbol) + MergedAppcoinsBalance(appcFiatValue, creditsBalance.second, creditsBalance.first.value) + }) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnNext { + val appcFiat = formatter.formatCurrency(it.appcFiatValue.amount, WalletCurrency.APPCOINS) + val creditsFiat = formatter.formatCurrency(it.creditsFiatBalance.amount, + WalletCurrency.CREDITS) + view.updateBalanceValues(appcFiat, creditsFiat, it.creditsFiatBalance.currency) + } + .observeOn(networkScheduler) + .flatMapSingle { + Single.zip(hasEnoughCredits(it.creditsAppcAmount), + mergedAppcoinsInteractor.hasAppcFunds(transactionBuilder), + BiFunction { hasCredits: Availability, hasAppc: Availability -> + Pair(hasCredits, hasAppc) + }) + } + .observeOn(viewScheduler) + .doOnNext { + view.setPaymentsInformation(it.first.isAvailable, it.first.disableReason, + it.second.isAvailable, it.second.disableReason) + view.toggleSkeletons(false) + } + .doOnSubscribe { view.toggleSkeletons(true) } + .subscribe({ }, { it.printStackTrace() })) + } + + private fun hasEnoughCredits(creditsAppcAmount: BigDecimal): Single { + return Single.fromCallable { + val available = creditsAppcAmount >= transactionBuilder.amount() + val disabledReason = + if (!available) R.string.purchase_appcoins_credits_noavailable_body else null + Availability(available, disabledReason) + } + } + + private fun handleBackClick() { + disposables.add(Observable.merge(view.backClick(), view.backPressed()) + .observeOn(networkScheduler) + .doOnNext { paymentMethod -> + analytics.sendPaymentConfirmationEvent(paymentMethod.packageName, + paymentMethod.skuDetails, paymentMethod.value, paymentMethod.purchaseDetails, + paymentMethod.transactionType, "cancel") + } + .observeOn(viewScheduler) + .doOnNext { view.showPaymentMethodsView() } + .subscribe({}, { showError(it) })) + } + + private fun handleAuthenticationResult() { + disposables.add(view.onAuthenticationResult() + .observeOn(viewScheduler) + .doOnNext { + if (!it || cachedSelectedPaymentId == null) { + view.hideLoading() + } else { + navigateToPayment(cachedSelectedPaymentId!!) + } + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun navigateToPayment(selectedPaymentId: String) { + when (paymentMethodsMapper.map(selectedPaymentId)) { + PaymentMethodsView.SelectedPaymentMethod.APPC -> view.navigateToAppcPayment() + PaymentMethodsView.SelectedPaymentMethod.APPC_CREDITS -> view.navigateToCreditsPayment() + else -> { + view.showError(R.string.unknown_error) + logger.log(TAG, "Wrong payment method after authentication.") + } + } + } + + private fun handleBuyClick() { + disposables.add(view.buyClick() + .observeOn(networkScheduler) + .doOnNext { paymentMethod -> + analytics.sendPaymentConfirmationEvent(paymentMethod.packageName, + paymentMethod.skuDetails, paymentMethod.value, paymentMethod.purchaseDetails, + paymentMethod.transactionType, "buy") + } + .observeOn(viewScheduler) + .doOnNext { view.showLoading() } + .flatMapSingle { paymentMethod -> + mergedAppcoinsInteractor.isWalletBlocked() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { + if (mergedAppcoinsInteractor.hasAuthenticationPermission()) { + cachedSelectedPaymentId = map(paymentMethod.purchaseDetails) + view.showAuthenticationActivity() + } else { + handleBuyClickSelection(paymentMethod.purchaseDetails) + } + } + } + .subscribe({}, { + view.hideLoading() + showError(it) + })) + } + + private fun map(purchaseDetails: String): String { + if (purchaseDetails == APPC) return PaymentMethodsView.PaymentMethodId.APPC.id + else if (purchaseDetails == CREDITS) return PaymentMethodsView.PaymentMethodId.APPC_CREDITS.id + return "" + } + + private fun handleSupportClicks() { + disposables.add(Observable.merge(view.getSupportIconClicks(), view.getSupportLogoClicks()) + .throttleFirst(50, TimeUnit.MILLISECONDS) + .flatMapCompletable { mergedAppcoinsInteractor.showSupport(gamificationLevel) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleErrorDismiss() { + disposables.add(view.errorDismisses() + .observeOn(viewScheduler) + .doOnNext { navigator.popViewWithError() } + .subscribe({}, { + it.printStackTrace() + navigator.popViewWithError() + })) + } + + private fun handlePaymentSelectionChange() { + disposables.add(view.getPaymentSelection() + .doOnNext { handleSelection(it) } + .subscribe({}, { showError(it) })) + } + + private fun showError(t: Throwable) { + logger.log(TAG, t) + if (t.isNoNetworkException()) { + view.showError(R.string.notification_no_network_poa) + } else { + view.showError(R.string.activity_iab_error_message) + } + } + + private fun handleBuyClickSelection(selection: String) { + when (selection) { + APPC -> view.navigateToAppcPayment() + CREDITS -> view.navigateToCreditsPayment() + else -> Log.w(TAG, "No appcoins payment method selected") + } + } + + private fun handleSelection(selection: String) { + when (selection) { + APPC -> view.showBonus() + CREDITS -> view.hideBonus() + else -> Log.w(TAG, "Error creating PublishSubject") + } + } + + private fun getCreditsBalance(): Observable> = + mergedAppcoinsInteractor.getCreditsBalance() + + private fun getAppcBalance(): Observable = mergedAppcoinsInteractor.getAppcBalance() + + private fun getEthBalance(): Observable = mergedAppcoinsInteractor.getEthBalance() + + fun onSavedInstanceState(outState: Bundle) { + outState.putString(SELECTED_PAYMENT_ID, cachedSelectedPaymentId) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsView.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsView.kt new file mode 100644 index 00000000000..a0110bcd772 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/MergedAppcoinsView.kt @@ -0,0 +1,48 @@ +package com.asfoundation.wallet.ui.iab + +import androidx.annotation.StringRes +import io.reactivex.Observable + +interface MergedAppcoinsView { + + fun showError(@StringRes errorMessage: Int) + + fun getPaymentSelection(): Observable + + fun hideBonus() + + fun showBonus() + + fun buyClick(): Observable + + fun backClick(): Observable + + fun backPressed(): Observable + + fun navigateToAppcPayment() + + fun navigateToCreditsPayment() + + fun updateBalanceValues(appcFiat: String, creditsFiat: String, currency: String) + + fun showLoading() + + fun hideLoading() + + fun errorDismisses(): Observable + + fun getSupportLogoClicks(): Observable + + fun getSupportIconClicks(): Observable + + fun setPaymentsInformation(hasCredits: Boolean, creditsDisableReason: Int?, hasAppc: Boolean, + appcDisabledReason: Int?) + + fun toggleSkeletons(show: Boolean) + + fun showAuthenticationActivity() + + fun onAuthenticationResult(): Observable + + fun showPaymentMethodsView() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/Navigator.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/Navigator.java new file mode 100644 index 00000000000..261c06764d1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/Navigator.java @@ -0,0 +1,16 @@ +package com.asfoundation.wallet.ui.iab; + +import android.net.Uri; +import android.os.Bundle; +import io.reactivex.Observable; + +public interface Navigator { + + void popView(Bundle bundle); + + void popViewWithError(); + + void navigateToUriForResult(String redirectUrl); + + Observable uriResults(); +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/NotEnoughFundsException.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/NotEnoughFundsException.java new file mode 100644 index 00000000000..62913f86232 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/NotEnoughFundsException.java @@ -0,0 +1,4 @@ +package com.asfoundation.wallet.ui.iab; + +class NotEnoughFundsException extends Throwable { +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/OnChainBuyFragment.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/OnChainBuyFragment.java new file mode 100644 index 00000000000..fe9be0f8969 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/OnChainBuyFragment.java @@ -0,0 +1,299 @@ +package com.asfoundation.wallet.ui.iab; + +import android.content.Context; +import android.graphics.Typeface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import com.airbnb.lottie.FontAssetDelegate; +import com.airbnb.lottie.LottieAnimationView; +import com.airbnb.lottie.TextDelegate; +import com.asf.wallet.R; +import com.asfoundation.wallet.billing.analytics.BillingAnalytics; +import com.asfoundation.wallet.entity.TransactionBuilder; +import com.asfoundation.wallet.logging.Logger; +import com.jakewharton.rxbinding2.view.RxView; +import dagger.android.support.DaggerFragment; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import javax.inject.Inject; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import static com.asfoundation.wallet.ui.iab.IabActivity.PRODUCT_NAME; +import static com.asfoundation.wallet.ui.iab.IabActivity.TRANSACTION_AMOUNT; + +/** + * Created by franciscocalado on 19/07/2018. + */ + +public class OnChainBuyFragment extends DaggerFragment implements OnChainBuyView { + + private static final String APP_PACKAGE = "app_package"; + private static final String TRANSACTION_BUILDER_KEY = "transaction_builder"; + private static final String BONUS_KEY = "bonus"; + private static final String GAMIFICATION_LEVEL = "gamification_level"; + @Inject OnChainBuyInteract onChainBuyInteract; + @Inject BillingAnalytics analytics; + @Inject Logger logger; + private Button okErrorButton; + private OnChainBuyPresenter presenter; + private View loadingView; + private View transactionCompletedLayout; + private View transactionErrorLayout; + private TextView errorTextView; + private TextView loadingMessage; + private ArrayAdapter adapter; + private IabView iabView; + private Bundle extras; + private String data; + private boolean isBds; + private TransactionBuilder transaction; + private LottieAnimationView lottieTransactionComplete; + private View supportIcon; + private View supportLogo; + private int gamificationLevel; + + public static OnChainBuyFragment newInstance(Bundle extras, String data, boolean bdsIap, + TransactionBuilder transaction, String bonus, int gamificationLevel) { + OnChainBuyFragment fragment = new OnChainBuyFragment(); + Bundle bundle = new Bundle(); + bundle.putBundle("extras", extras); + bundle.putString("data", data); + bundle.putBoolean("isBds", bdsIap); + bundle.putParcelable(TRANSACTION_BUILDER_KEY, transaction); + bundle.putString(BONUS_KEY, bonus); + bundle.putInt(GAMIFICATION_LEVEL, gamificationLevel); + fragment.setArguments(bundle); + return fragment; + } + + @Override public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + extras = getArguments().getBundle("extras"); + data = getArguments().getString("data"); + isBds = getArguments().getBoolean("isBds"); + transaction = getArguments().getParcelable(TRANSACTION_BUILDER_KEY); + gamificationLevel = getArguments().getInt(GAMIFICATION_LEVEL); + } + + @Override + public View onCreateView(@NotNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + return inflater.inflate(R.layout.fragment_iab, container, false); + } + + @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + + okErrorButton = view.findViewById(R.id.error_dismiss); + loadingView = view.findViewById(R.id.loading); + loadingMessage = view.findViewById(R.id.loading_message); + errorTextView = view.findViewById(R.id.error_message); + transactionCompletedLayout = view.findViewById(R.id.iab_activity_transaction_completed); + transactionErrorLayout = view.findViewById(R.id.generic_purchase_error_layout); + + supportIcon = view.findViewById(R.id.layout_support_icn); + supportLogo = view.findViewById(R.id.layout_support_logo); + okErrorButton.setText(R.string.ok); + + lottieTransactionComplete = + transactionCompletedLayout.findViewById(R.id.lottie_transaction_success); + + presenter = new OnChainBuyPresenter(this, AndroidSchedulers.mainThread(), Schedulers.io(), + new CompositeDisposable(), onChainBuyInteract.getBillingMessagesMapper(), isBds, analytics, + getAppPackage(), data, gamificationLevel, logger, onChainBuyInteract); + adapter = + new ArrayAdapter<>(getContext().getApplicationContext(), R.layout.iab_raiden_dropdown_item, + R.id.item, new ArrayList<>()); + + presenter.present(extras.getString(PRODUCT_NAME, ""), + (BigDecimal) extras.getSerializable(TRANSACTION_AMOUNT), transaction.getPayload()); + + if (StringUtils.isNotBlank(getBonus())) { + lottieTransactionComplete.setAnimation(R.raw.transaction_complete_bonus_animation); + setupTransactionCompleteAnimation(); + } else { + lottieTransactionComplete.setAnimation(R.raw.success_animation); + } + } + + @Override public void onResume() { + super.onResume(); + presenter.resume(); + } + + @Override public void onPause() { + presenter.pause(); + super.onPause(); + } + + @Override public void onDestroyView() { + presenter.stop(); + lottieTransactionComplete.removeAllAnimatorListeners(); + lottieTransactionComplete.removeAllUpdateListeners(); + lottieTransactionComplete.removeAllLottieOnCompositionLoadedListener(); + lottieTransactionComplete = null; + super.onDestroyView(); + } + + @Override public void onDetach() { + super.onDetach(); + iabView = null; + } + + @Override public @NotNull Observable getOkErrorClick() { + return RxView.clicks(okErrorButton); + } + + @Override public @NotNull Observable getSupportIconClick() { + return RxView.clicks(supportIcon); + } + + @Override public @NotNull Observable getSupportLogoClick() { + return RxView.clicks(supportLogo); + } + + @Override public void close(Bundle data) { + iabView.close(data); + } + + @Override public void finish(Bundle data) { + presenter.sendPaymentEvent(); + presenter.sendRevenueEvent(); + presenter.sendPaymentSuccessEvent(); + data.putString(InAppPurchaseInteractor.PRE_SELECTED_PAYMENT_METHOD_KEY, + PaymentMethodsView.PaymentMethodId.APPC.getId()); + iabView.finish(data); + } + + @Override public void showError() { + showError(R.string.activity_iab_error_message); + } + + @Override public void showTransactionCompleted() { + iabView.lockRotation(); + loadingView.setVisibility(View.GONE); + transactionErrorLayout.setVisibility(View.GONE); + transactionCompletedLayout.setVisibility(View.VISIBLE); + } + + @Override public void showWrongNetworkError() { + showError(R.string.activity_iab_wrong_network_message); + } + + @Override public void showNoNetworkError() { + showError(R.string.activity_iab_no_network_message); + } + + @Override public void showApproving() { + showLoading(R.string.activity_iab_approving_message); + } + + @Override public void showBuying() { + showLoading(R.string.activity_iab_buying_message); + } + + @Override public void showNonceError() { + showError(R.string.activity_iab_nonce_message); + } + + @Override public void showNoTokenFundsError() { + showError(R.string.activity_iab_no_token_funds_message); + } + + @Override public void showNoEtherFundsError() { + showError(R.string.activity_iab_no_ethereum_funds_message); + } + + @Override public void showNoFundsError() { + showError(R.string.activity_iab_no_funds_message); + } + + @Override public void showForbiddenError() { + showError(R.string.purchase_error_wallet_block_code_403); + } + + @Override public void showRaidenChannelValues(@NotNull List values) { + adapter.clear(); + adapter.addAll(values); + adapter.notifyDataSetChanged(); + } + + @Override public long getAnimationDuration() { + return lottieTransactionComplete.getDuration(); + } + + @Override public void lockRotation() { + iabView.lockRotation(); + } + + @Override public void showWalletValidation(@StringRes int error) { + iabView.showWalletValidation(error); + } + + @Override public void onAttach(Context context) { + super.onAttach(context); + if (!(context instanceof IabView)) { + throw new IllegalStateException("On chain buy fragment must be attached to IAB activity"); + } + iabView = ((IabView) context); + } + + private void showLoading(@StringRes int message) { + loadingView.setVisibility(View.VISIBLE); + transactionErrorLayout.setVisibility(View.GONE); + transactionCompletedLayout.setVisibility(View.GONE); + loadingMessage.setText(message); + loadingView.requestFocus(); + loadingView.setOnTouchListener((v, event) -> true); + } + + private void showError(int error_message) { + loadingView.setVisibility(View.GONE); + transactionErrorLayout.setVisibility(View.VISIBLE); + transactionCompletedLayout.setVisibility(View.GONE); + errorTextView.setText(error_message); + } + + private String getAppPackage() { + if (extras.containsKey(APP_PACKAGE)) { + return extras.getString(APP_PACKAGE); + } + throw new IllegalArgumentException("previous app package name not found"); + } + + private String getBonus() { + if (getArguments().containsKey(BONUS_KEY)) { + return getArguments().getString(BONUS_KEY); + } else { + throw new IllegalArgumentException("bonus amount data not found"); + } + } + + private void setupTransactionCompleteAnimation() { + LottieAnimationView lottieTransactionComplete = + transactionCompletedLayout.findViewById(R.id.lottie_transaction_success); + TextDelegate textDelegate = new TextDelegate(lottieTransactionComplete); + textDelegate.setText("bonus_value", getBonus()); + textDelegate.setText("bonus_received", + getResources().getString(R.string.gamification_purchase_completed_bonus_received)); + lottieTransactionComplete.setTextDelegate(textDelegate); + lottieTransactionComplete.setFontAssetDelegate(new FontAssetDelegate() { + @Override public Typeface fetchFont(String fontFamily) { + return Typeface.create("sans-serif-medium", Typeface.BOLD); + } + }); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/OnChainBuyInteract.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/OnChainBuyInteract.kt new file mode 100644 index 00000000000..cf01dc14632 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/OnChainBuyInteract.kt @@ -0,0 +1,76 @@ +package com.asfoundation.wallet.ui.iab + +import com.appcoins.wallet.bdsbilling.WalletService +import com.appcoins.wallet.billing.BillingMessagesMapper +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.ui.iab.AsfInAppPurchaseInteractor.CurrentPaymentStep +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +import java.math.BigDecimal +import java.util.* + +class OnChainBuyInteract(private val inAppPurchaseInteractor: InAppPurchaseInteractor, + private val supportInteractor: SupportInteractor, + private val walletService: WalletService, + private val walletBlockedInteract: WalletBlockedInteract, + private val smsValidationInteract: SmsValidationInteract) { + + fun showSupport(gamificationLevel: Int): Completable { + return walletService.getWalletAddress() + .flatMapCompletable { + Completable.fromAction { + supportInteractor.registerUser(gamificationLevel, it.toLowerCase(Locale.ROOT)) + supportInteractor.displayChatScreen() + } + } + } + + fun isWalletBlocked() = walletBlockedInteract.isWalletBlocked() + + fun isWalletVerified() = + walletService.getWalletAddress() + .flatMap { smsValidationInteract.isValidated(it) } + .onErrorReturn { true } + + fun getTransactionState(uri: String?): Observable = + inAppPurchaseInteractor.getTransactionState(uri) + + fun send(uri: String?, transactionType: AsfInAppPurchaseInteractor.TransactionType, + packageName: String, productName: String?, developerPayload: String?, + isBds: Boolean): Completable { + return inAppPurchaseInteractor.send(uri, transactionType, packageName, productName, + developerPayload, isBds) + } + + fun parseTransaction(uri: String?, isBds: Boolean): Single = + inAppPurchaseInteractor.parseTransaction(uri, isBds) + + fun getCurrentPaymentStep(packageName: String, + transactionBuilder: TransactionBuilder): Single = + inAppPurchaseInteractor.getCurrentPaymentStep(packageName, transactionBuilder) + + fun resume(uri: String?, transactionType: AsfInAppPurchaseInteractor.TransactionType, + packageName: String, productName: String?, developerPayload: String?, + isBds: Boolean): Completable { + return inAppPurchaseInteractor.resume(uri, transactionType, packageName, productName, + developerPayload, isBds) + } + + fun getCompletedPurchase(transaction: Payment, isBds: Boolean): Single = + inAppPurchaseInteractor.getCompletedPurchase(transaction, isBds) + + fun remove(uri: String?): Completable = inAppPurchaseInteractor.remove(uri) + + fun getTopUpChannelSuggestionValues(price: BigDecimal): List = + inAppPurchaseInteractor.getTopUpChannelSuggestionValues(price) + + fun convertToFiat(appcValue: Double, currency: String): Single = + inAppPurchaseInteractor.convertToFiat(appcValue, currency) + + fun getBillingMessagesMapper(): BillingMessagesMapper = + inAppPurchaseInteractor.billingMessagesMapper +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/OnChainBuyPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/OnChainBuyPresenter.kt new file mode 100644 index 00000000000..d5cdfb77895 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/OnChainBuyPresenter.kt @@ -0,0 +1,278 @@ +package com.asfoundation.wallet.ui.iab + +import android.os.Bundle +import com.appcoins.wallet.billing.BillingMessagesMapper +import com.asf.wallet.R +import com.asfoundation.wallet.analytics.FacebookEventLogger +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.ui.iab.AsfInAppPurchaseInteractor.CurrentPaymentStep +import com.asfoundation.wallet.util.UnknownTokenException +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import java.math.BigDecimal +import java.util.concurrent.TimeUnit + +class OnChainBuyPresenter(private val view: OnChainBuyView, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val billingMessagesMapper: BillingMessagesMapper, + private val isBds: Boolean, + private val analytics: BillingAnalytics, + private val appPackage: String, + private val uriString: String?, + private val gamificationLevel: Int, + private val logger: Logger, + private val onChainBuyInteract: OnChainBuyInteract) { + + private val transactionBuilder = onChainBuyInteract.parseTransaction(uriString, isBds) + private var statusDisposable: Disposable? = null + + fun present(productName: String?, amount: BigDecimal, developerPayload: String?) { + setupUi(amount, developerPayload) + handleOkErrorClick() + handleBuyEvent(productName, developerPayload, isBds) + handleSupportClick() + } + + private fun showTransactionState() { + if (statusDisposable != null && !statusDisposable!!.isDisposed) { + statusDisposable!!.dispose() + } + statusDisposable = onChainBuyInteract.getTransactionState(uriString) + .observeOn(viewScheduler) + .flatMapCompletable { showPendingTransaction(it) } + .subscribe({}) { showError(it) } + } + + private fun handleBuyEvent(productName: String?, developerPayload: String?, isBds: Boolean) { + showTransactionState() + disposables.add( + onChainBuyInteract.send(uriString, AsfInAppPurchaseInteractor.TransactionType.NORMAL, + appPackage, productName, developerPayload, isBds) + .observeOn(viewScheduler) + .doOnError { showError(it) } + .subscribe({}, { showError(it) })) + } + + private fun handleOkErrorClick() { + disposables.add(view.getOkErrorClick() + .flatMapSingle { onChainBuyInteract.parseTransaction(uriString, isBds) } + .subscribe({ close() }, { close() })) + } + + private fun handleSupportClick() { + disposables.add(Observable.merge(view.getSupportIconClick(), view.getSupportLogoClick()) + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .flatMapCompletable { onChainBuyInteract.showSupport(gamificationLevel) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun setupUi(appcAmount: BigDecimal, developerPayload: String?) { + disposables.add(onChainBuyInteract.parseTransaction(uriString, isBds) + .flatMapCompletable { transaction: TransactionBuilder -> + onChainBuyInteract.getCurrentPaymentStep(appPackage, transaction) + .flatMapCompletable { currentPaymentStep: CurrentPaymentStep -> + when (currentPaymentStep) { + CurrentPaymentStep.PAUSED_ON_CHAIN -> onChainBuyInteract.resume(uriString, + AsfInAppPurchaseInteractor.TransactionType.NORMAL, appPackage, + transaction.skuId, developerPayload, isBds) + + CurrentPaymentStep.READY -> Completable.fromAction { setup(appcAmount) } + .subscribeOn(viewScheduler) + + CurrentPaymentStep.NO_FUNDS -> Completable.fromAction { view.showNoFundsError() } + .subscribeOn(viewScheduler) + + CurrentPaymentStep.PAUSED_CC_PAYMENT, CurrentPaymentStep.PAUSED_LOCAL_PAYMENT, CurrentPaymentStep.PAUSED_CREDITS -> + Completable.error(UnsupportedOperationException( + "Cannot resume from " + currentPaymentStep.name + " status")) + } + } + } + .subscribe({}) { showError(it) }) + } + + private fun close() = view.close(billingMessagesMapper.mapCancellation()) + + private fun showError(throwable: Throwable?, message: String? = null) { + logger.log(TAG, message, throwable) + if (throwable is UnknownTokenException) view.showWrongNetworkError() + else view.showError() + } + + private fun showPendingTransaction(transaction: Payment): Completable { + sendPaymentErrorEvent(transaction) + return when (transaction.status) { + Payment.Status.COMPLETED -> { + view.lockRotation() + onChainBuyInteract.getCompletedPurchase(transaction, isBds) + .observeOn(viewScheduler) + .map { buildBundle(it, transaction.orderReference) } + .flatMapCompletable { bundle -> handleSuccessTransaction(bundle) } + .onErrorResumeNext { Completable.fromAction { showError(it) } } + } + Payment.Status.NO_FUNDS -> Completable.fromAction { view.showNoFundsError() } + .andThen(onChainBuyInteract.remove(transaction.uri)) + + Payment.Status.NETWORK_ERROR -> Completable.fromAction { view.showWrongNetworkError() } + .andThen(onChainBuyInteract.remove(transaction.uri)) + + Payment.Status.NO_TOKENS -> Completable.fromAction { view.showNoTokenFundsError() } + .andThen(onChainBuyInteract.remove(transaction.uri)) + + Payment.Status.NO_ETHER -> Completable.fromAction { view.showNoEtherFundsError() } + .andThen(onChainBuyInteract.remove(transaction.uri)) + + Payment.Status.NO_INTERNET -> Completable.fromAction { view.showNoNetworkError() } + .andThen(onChainBuyInteract.remove(transaction.uri)) + + Payment.Status.NONCE_ERROR -> Completable.fromAction { view.showNonceError() } + .andThen(onChainBuyInteract.remove(transaction.uri)) + + Payment.Status.APPROVING -> { + view.lockRotation() + Completable.fromAction { view.showApproving() } + } + + Payment.Status.BUYING -> { + view.lockRotation() + Completable.fromAction { view.showBuying() } + } + Payment.Status.FORBIDDEN -> Completable.fromAction { handleFraudFlow() } + .andThen(onChainBuyInteract.remove(transaction.uri)) + + Payment.Status.ERROR -> Completable.fromAction { + showError(null, "Payment status: ${transaction.status.name}") + } + .andThen(onChainBuyInteract.remove(transaction.uri)) + + else -> Completable.fromAction { + showError(null, "Payment status: UNKNOWN") + } + .andThen(onChainBuyInteract.remove(transaction.uri)) + } + } + + private fun handleSuccessTransaction(bundle: Bundle): Completable { + return Completable.fromAction { view.showTransactionCompleted() } + .subscribeOn(viewScheduler) + .andThen(Completable.timer(view.getAnimationDuration(), TimeUnit.MILLISECONDS)) + .andThen(Completable.fromRunnable { view.finish(bundle) }) + } + + private fun buildBundle(payment: Payment, orderReference: String?): Bundle { + return if (payment.uid != null && payment.signature != null && payment.signatureData != null) { + billingMessagesMapper.mapPurchase(payment.uid!!, payment.signature!!, + payment.signatureData!!, orderReference) + } else { + Bundle().also { + it.putInt(IabActivity.RESPONSE_CODE, 0) + it.putString(IabActivity.TRANSACTION_HASH, payment.buyHash) + } + } + } + + fun stop() = disposables.clear() + + private fun setup(amount: BigDecimal) = + view.showRaidenChannelValues(onChainBuyInteract.getTopUpChannelSuggestionValues(amount)) + + fun sendPaymentEvent() { + disposables.add(transactionBuilder.subscribe { transactionBuilder: TransactionBuilder -> + analytics.sendPaymentEvent(appPackage, transactionBuilder.skuId, + transactionBuilder.amount() + .toString(), BillingAnalytics.PAYMENT_METHOD_APPC, transactionBuilder.type) + }) + } + + fun resume() = showTransactionState() + + fun pause() = statusDisposable?.dispose() + + fun sendRevenueEvent() { + disposables.add(transactionBuilder.flatMap { transaction -> + onChainBuyInteract.convertToFiat(transaction.amount() + .toDouble(), FacebookEventLogger.EVENT_REVENUE_CURRENCY) + } + .doOnSuccess { (amount) -> analytics.sendRevenueEvent(amount.toString()) } + .subscribe({ }, { it.printStackTrace() })) + } + + fun sendPaymentSuccessEvent() { + disposables.add(transactionBuilder.observeOn(networkScheduler) + .subscribe { transaction -> + analytics.sendPaymentSuccessEvent(appPackage, transaction.skuId, transaction.amount() + .toString(), BillingAnalytics.PAYMENT_METHOD_APPC, transaction.type) + }) + } + + private fun sendPaymentErrorEvent(payment: Payment) { + val status = payment.status + if (isError(status)) { + if (payment.errorCode == null && payment.errorMessage == null) { + disposables.add(transactionBuilder.observeOn(networkScheduler) + .subscribe { transaction -> + analytics.sendPaymentErrorEvent(appPackage, transaction.skuId, transaction.amount() + .toString(), BillingAnalytics.PAYMENT_METHOD_APPC, transaction.type, + status.name) + }) + } else { + disposables.add(transactionBuilder.observeOn(networkScheduler) + .subscribe { transaction -> + analytics.sendPaymentErrorWithDetailsEvent(appPackage, transaction.skuId, + transaction.amount() + .toString(), BillingAnalytics.PAYMENT_METHOD_APPC, transaction.type, + payment.errorCode.toString(), payment.errorMessage.toString()) + }) + } + } + } + + private fun handleFraudFlow() { + disposables.add(onChainBuyInteract.isWalletBlocked() + .subscribeOn(networkScheduler) + .observeOn(networkScheduler) + .flatMap { blocked -> + if (blocked) { + onChainBuyInteract.isWalletVerified() + .observeOn(viewScheduler) + .doOnSuccess { verified -> + if (verified) { + view.showForbiddenError() + } else { + view.showWalletValidation(R.string.purchase_error_wallet_block_code_403) + } + } + } else { + Single.just(true) + .observeOn(viewScheduler) + .doOnSuccess { view.showForbiddenError() } + } + } + .observeOn(viewScheduler) + .subscribe({}, { + logger.log(TAG, it) + view.showForbiddenError() + })) + } + + + private fun isError(status: Payment.Status): Boolean { + return status == Payment.Status.ERROR || status == Payment.Status.NO_FUNDS || + status == Payment.Status.NONCE_ERROR || status == Payment.Status.NO_ETHER || + status == Payment.Status.NO_INTERNET || status == Payment.Status.NO_TOKENS || + status == Payment.Status.NETWORK_ERROR || status == Payment.Status.FORBIDDEN + } + + companion object { + private val TAG = OnChainBuyPresenter::class.java.simpleName + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/OnChainBuyView.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/OnChainBuyView.kt new file mode 100644 index 00000000000..67a32758635 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/OnChainBuyView.kt @@ -0,0 +1,49 @@ +package com.asfoundation.wallet.ui.iab + +import android.os.Bundle +import androidx.annotation.StringRes +import io.reactivex.Observable +import java.math.BigDecimal + +interface OnChainBuyView { + + fun getOkErrorClick(): Observable + + fun getSupportIconClick(): Observable + + fun getSupportLogoClick(): Observable + + fun close(data: Bundle?) + + fun finish(data: Bundle?) + + fun showError() + + fun showTransactionCompleted() + + fun showWrongNetworkError() + + fun showNoNetworkError() + + fun showApproving() + + fun showBuying() + + fun showNonceError() + + fun showNoTokenFundsError() + + fun showNoEtherFundsError() + + fun showNoFundsError() + + fun showForbiddenError() + + fun showRaidenChannelValues(values: List) + + fun getAnimationDuration(): Long + + fun lockRotation() + + fun showWalletValidation(@StringRes error: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/Payment.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/Payment.java new file mode 100644 index 00000000000..c5e86cb890b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/Payment.java @@ -0,0 +1,147 @@ +package com.asfoundation.wallet.ui.iab; + +import javax.annotation.Nullable; + +public class Payment { + private final Status status; + private final String uri; + private @Nullable final String fromAddress; + private @Nullable final String buyHash; + private @Nullable final String packageName; + private @Nullable final String productName; + private @Nullable final String uid; + private @Nullable final String signature; + private @Nullable final String signatureData; + private @Nullable final String productId; + private @Nullable final String orderReference; + private @Nullable final Integer errorCode; + private @Nullable final String errorMessage; + + public Payment(String uri, Status status, @Nullable String uid, @Nullable String signature, + @Nullable String signatureData, @Nullable String orderReference, @Nullable Integer errorCode, + @Nullable String errorMessage) { + this.status = status; + this.uri = uri; + this.fromAddress = null; + this.buyHash = null; + this.packageName = null; + this.productName = null; + this.uid = uid; + this.signature = signature; + this.signatureData = signatureData; + this.productId = null; + this.orderReference = orderReference; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } + + public Payment(String uri, Status status, @Nullable String fromAddress, @Nullable String buyHash, + @Nullable String packageName, @Nullable String productName, @Nullable String productId, + @Nullable String orderReference, @Nullable Integer errorCode, @Nullable String errorMessage) { + this.status = status; + this.uri = uri; + this.fromAddress = fromAddress; + this.buyHash = buyHash; + this.packageName = packageName; + this.productName = productName; + this.uid = null; + this.signature = null; + this.signatureData = null; + this.productId = productId; + this.orderReference = orderReference; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } + + @Nullable public String getOrderReference() { + return orderReference; + } + + @Nullable public String getFromAddress() { + return fromAddress; + } + + public Status getStatus() { + return status; + } + + public String getUri() { + return uri; + } + + public @Nullable String getBuyHash() { + return buyHash; + } + + public @Nullable String getPackageName() { + return packageName; + } + + public @Nullable String getProductName() { + return productName; + } + + public @Nullable String getProductId() { + return productId; + } + + @Nullable public String getSignatureData() { + return signatureData; + } + + @Nullable public String getUid() { + return uid; + } + + @Nullable public String getSignature() { + return signature; + } + + @Nullable public Integer getErrorCode() { + return errorCode; + } + + @Nullable public String getErrorMessage() { + return errorMessage; + } + + @Override public String toString() { + return "Payment{" + + "status=" + + status + + ", uri='" + + uri + + '\'' + + ", fromAddress='" + + fromAddress + + '\'' + + ", buyHash='" + + buyHash + + '\'' + + ", packageName='" + + packageName + + '\'' + + ", productName='" + + productName + + '\'' + + ", uid='" + + uid + + '\'' + + ", signature='" + + signature + + '\'' + + ", signatureData='" + + signatureData + + '\'' + + ", productId='" + + productId + + '\'' + + '}'; + } + + public enum Status { + COMPLETED, NO_FUNDS, NETWORK_ERROR, NO_ETHER, NO_TOKENS, NO_INTERNET, NONCE_ERROR, APPROVING, + BUYING, FORBIDDEN, ERROR + } +} + diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentAuthenticationResult.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentAuthenticationResult.kt new file mode 100644 index 00000000000..d7e8660a19b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentAuthenticationResult.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.ui.iab + +import com.asfoundation.wallet.ui.PaymentNavigationData + +data class PaymentAuthenticationResult(val isSuccess: Boolean, + val paymentNavigationData: PaymentNavigationData?) diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentInfoWrapper.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentInfoWrapper.kt new file mode 100644 index 00000000000..661abb9d1f9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentInfoWrapper.kt @@ -0,0 +1,4 @@ +package com.asfoundation.wallet.ui.iab + +data class PaymentInfoWrapper(val packageName: String, val skuDetails: String?, val value: String, + val purchaseDetails: String, val transactionType: String) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethod.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethod.kt new file mode 100644 index 00000000000..df3cf909120 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethod.kt @@ -0,0 +1,27 @@ +package com.asfoundation.wallet.ui.iab + +import java.math.BigDecimal + +open class PaymentMethod(open val id: String, open val label: String, + open val iconUrl: String, val fee: PaymentMethodFee?, + open val isEnabled: Boolean = true, open var disabledReason: Int? = null) { + constructor() : this("", "", "", null, false) + + companion object { + @JvmField + val APPC: PaymentMethod = + PaymentMethod("appcoins", "AppCoins (APPC)", + "https://cdn6.aptoide.com/imgs/a/f/9/af95bd0d14875800231f05dbf1933143_logo.png", + null) + } +} + +data class PaymentMethodFee( + val isExact: Boolean, + val amount: BigDecimal?, + val currency: String? +) { + + fun isValidFee() = isExact && amount != null && currency != null + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsAdapter.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsAdapter.kt new file mode 100644 index 00000000000..40132ffe982 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsAdapter.kt @@ -0,0 +1,41 @@ +package com.asfoundation.wallet.ui.iab + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R +import com.jakewharton.rxrelay2.PublishRelay + + +class PaymentMethodsAdapter( + private var paymentMethods: List, + private var paymentMethodId: String, + private var paymentMethodClick: PublishRelay) : + RecyclerView.Adapter() { + private var selectedItem = -1 + + init { + paymentMethods.forEachIndexed { index, paymentMethod -> + if (paymentMethod.id == paymentMethodId) selectedItem = index + } + } + + fun getSelectedItem() = selectedItem + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentMethodsViewHolder { + return PaymentMethodsViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_payment_method, parent, false)) + } + + override fun getItemCount() = paymentMethods.size + + override fun onBindViewHolder(holder: PaymentMethodsViewHolder, position: Int) { + holder.bind(paymentMethods[position], selectedItem == position, View.OnClickListener { + selectedItem = position + paymentMethodClick.accept(position) + notifyDataSetChanged() + }) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsAnalytics.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsAnalytics.kt new file mode 100644 index 00000000000..40b3bb6d8fa --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsAnalytics.kt @@ -0,0 +1,32 @@ +package com.asfoundation.wallet.ui.iab + +import com.asfoundation.wallet.analytics.AmplitudeAnalytics +import com.asfoundation.wallet.analytics.RakamAnalytics +import com.asfoundation.wallet.billing.analytics.BillingAnalytics + +class PaymentMethodsAnalytics(private val billingAnalytics: BillingAnalytics, + private val rakamAnalytics: RakamAnalytics, + private val amplitudeAnalytics: AmplitudeAnalytics) { + + fun setGamificationLevel(cachedGamificationLevel: Int) { + rakamAnalytics.setGamificationLevel(cachedGamificationLevel) + amplitudeAnalytics.setGamificationLevel(cachedGamificationLevel) + } + + fun sendPurchaseDetailsEvent(appPackage: String, skuId: String?, amount: String, + type: String?) { + billingAnalytics.sendPurchaseDetailsEvent(appPackage, skuId, amount, type) + } + + fun sendPaymentMethodEvent(appPackage: String, skuId: String?, amount: String, + paymentId: String, type: String?, action: String, + isPreselected: Boolean = false) { + if (isPreselected) { + billingAnalytics.sendPreSelectedPaymentMethodEvent(appPackage, skuId, amount, paymentId, type, + action) + } else { + billingAnalytics.sendPaymentMethodEvent(appPackage, skuId, amount, paymentId, type, action) + + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsData.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsData.kt new file mode 100644 index 00000000000..cb9be8f4771 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsData.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.ui.iab + +import java.math.BigDecimal + +data class PaymentMethodsData(val appPackage: String, val isBds: Boolean, + val developerPayload: String?, + val uri: String?, val transactionValue: BigDecimal) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsFragment.kt new file mode 100644 index 00000000000..984dc0eee48 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsFragment.kt @@ -0,0 +1,603 @@ +package com.asfoundation.wallet.ui.iab + +import android.content.Context +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.util.Pair +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import com.asf.wallet.R +import com.asfoundation.wallet.GlideApp +import com.asfoundation.wallet.billing.adyen.PaymentType +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.ui.iab.PaymentMethodsView.PaymentMethodId +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxrelay2.PublishRelay +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.dialog_buy_buttons_payment_methods.* +import kotlinx.android.synthetic.main.iab_error_layout.* +import kotlinx.android.synthetic.main.iab_error_layout.view.* +import kotlinx.android.synthetic.main.payment_methods_header.* +import kotlinx.android.synthetic.main.payment_methods_layout.* +import kotlinx.android.synthetic.main.payment_methods_layout.error_message +import kotlinx.android.synthetic.main.selected_payment_method.* +import kotlinx.android.synthetic.main.support_error_layout.* +import kotlinx.android.synthetic.main.support_error_layout.view.error_message +import kotlinx.android.synthetic.main.view_purchase_bonus.* +import java.math.BigDecimal +import java.util.* +import javax.inject.Inject + +class PaymentMethodsFragment : DaggerFragment(), PaymentMethodsView { + + companion object { + private const val IS_BDS = "isBds" + private const val APP_PACKAGE = "app_package" + private const val TRANSACTION = "transaction" + private const val ITEM_ALREADY_OWNED = "item_already_owned" + private const val IS_DONATION = "is_donation" + + @JvmStatic + fun newInstance(transaction: TransactionBuilder?, productName: String?, + isBds: Boolean, isDonation: Boolean, + developerPayload: String?, uri: String?, + transactionData: String?): Fragment { + val bundle = Bundle() + bundle.apply { + putParcelable(TRANSACTION, transaction) + putSerializable(IabActivity.TRANSACTION_AMOUNT, transaction!!.amount()) + putString(APP_PACKAGE, transaction.domain) + putString(IabActivity.PRODUCT_NAME, productName) + putString(IabActivity.DEVELOPER_PAYLOAD, developerPayload) + putString(IabActivity.URI, uri) + putBoolean(IS_BDS, isBds) + putBoolean(IS_DONATION, isDonation) + putString(IabActivity.TRANSACTION_DATA, transactionData) + } + return PaymentMethodsFragment().apply { arguments = bundle } + } + } + + @Inject + lateinit var paymentMethodsAnalytics: PaymentMethodsAnalytics + + @Inject + lateinit var paymentMethodsMapper: PaymentMethodsMapper + + @Inject + lateinit var formatter: CurrencyFormatUtils + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var paymentMethodsInteractor: PaymentMethodsInteractor + + private lateinit var presenter: PaymentMethodsPresenter + private lateinit var iabView: IabView + private lateinit var compositeDisposable: CompositeDisposable + private lateinit var paymentMethodClick: PublishRelay + private lateinit var paymentMethodsAdapter: PaymentMethodsAdapter + private val paymentMethodList: MutableList = ArrayList() + private var setupSubject: PublishSubject? = null + private var preSelectedPaymentMethod: BehaviorSubject? = null + private var isPreSelected = false + private var itemAlreadyOwnedError = false + private var bonusMessageValue = "" + + override fun onAttach(context: Context) { + super.onAttach(context) + check(context is IabView) { "Payment Methods Fragment must be attached to IAB activity" } + iabView = context + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + compositeDisposable = CompositeDisposable() + setupSubject = PublishSubject.create() + preSelectedPaymentMethod = BehaviorSubject.create() + paymentMethodClick = PublishRelay.create() + itemAlreadyOwnedError = arguments?.getBoolean(ITEM_ALREADY_OWNED, false) ?: false + val paymentMethodsData = PaymentMethodsData(appPackage, isBds, getDeveloperPayload(), getUri(), + getTransactionValue()) + presenter = PaymentMethodsPresenter(this, AndroidSchedulers.mainThread(), + Schedulers.io(), CompositeDisposable(), paymentMethodsAnalytics, transactionBuilder!!, + paymentMethodsMapper, formatter, logger, paymentMethodsInteractor, paymentMethodsData) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + buy_button?.isEnabled = false + + setupAppNameAndIcon() + + setBuyButtonText() + presenter.present(savedInstanceState) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.payment_methods_layout, container, false) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(ITEM_ALREADY_OWNED, itemAlreadyOwnedError) + presenter.onSavedInstance(outState) + } + + override fun onDestroyView() { + presenter.stop() + compositeDisposable.clear() + super.onDestroyView() + } + + override fun showPaymentMethods(paymentMethods: MutableList, currency: String, + paymentMethodId: String, fiatAmount: String, appcAmount: String, + appcEnabled: Boolean, creditsEnabled: Boolean) { + updateHeaderInfo(currency, fiatAmount, appcAmount) + setupPaymentMethods(paymentMethods, paymentMethodId) + + setupSubject!!.onNext(true) + } + + override fun onResume() { + val firstRun = paymentMethodList.isEmpty() && !isPreSelected + presenter.onResume(firstRun) + super.onResume() + } + + private fun setupPaymentMethods(paymentMethods: MutableList, + paymentMethodId: String) { + isPreSelected = false + pre_selected_payment_method_group.visibility = View.GONE + mid_separator?.visibility = View.VISIBLE + if (paymentMethods.isNotEmpty()) { + paymentMethodsAdapter = + PaymentMethodsAdapter(paymentMethods, paymentMethodId, paymentMethodClick) + payment_methods_radio_list.adapter = paymentMethodsAdapter + paymentMethodList.clear() + paymentMethodList.addAll(paymentMethods) + paymentMethodClick.accept(paymentMethodsAdapter.getSelectedItem()) + } + } + + private fun updateHeaderInfo(currency: String, fiatAmount: String, appcAmount: String) { + val appcPrice = appcAmount + " " + WalletCurrency.APPCOINS.symbol + val fiatPrice = "$fiatAmount $currency" + appc_price.text = appcPrice + fiat_price.text = fiatPrice + fiat_price_skeleton.visibility = View.GONE + appc_price_skeleton.visibility = View.GONE + appc_price.visibility = View.VISIBLE + fiat_price.visibility = View.VISIBLE + } + + private fun getPaymentMethodLabel(paymentMethod: PaymentMethod): String { + return TranslatablePaymentMethods.values() + .firstOrNull { it.paymentMethod == paymentMethod.id } + ?.let { getString(it.stringId) } ?: paymentMethod.label + } + + override fun showPreSelectedPaymentMethod(paymentMethod: PaymentMethod, currency: String, + fiatAmount: String, appcAmount: String, + isBonusActive: Boolean) { + preSelectedPaymentMethod!!.onNext(paymentMethod) + updateHeaderInfo(currency, fiatAmount, appcAmount) + + setupPaymentMethod(paymentMethod, isBonusActive) + + setupSubject!!.onNext(true) + } + + private fun setupPaymentMethod(paymentMethod: PaymentMethod, + isBonusActive: Boolean) { + isPreSelected = true + mid_separator?.visibility = View.INVISIBLE + payment_method_description.visibility = View.VISIBLE + payment_method_description.text = getPaymentMethodLabel(paymentMethod) + payment_method_description_single.visibility = View.GONE + if (paymentMethod.id == PaymentMethodId.APPC_CREDITS.id) { + payment_method_secondary.visibility = View.VISIBLE + if (isBonusActive) hideBonus() + } else { + payment_method_secondary.visibility = View.GONE + if (isBonusActive) showBonus() + } + setupFee(paymentMethod.fee) + loadIcons(paymentMethod, payment_method_ic) + } + + private fun setupFee(fee: PaymentMethodFee?) { + if (fee?.isValidFee() == true) { + payment_method_fee.visibility = View.VISIBLE + val formattedValue = formatter.formatCurrency(fee.amount!!, WalletCurrency.FIAT) + payment_method_fee_value.text = "$formattedValue ${fee.currency}" + + payment_method_fee_value.apply { + this.setTextColor(ContextCompat.getColor(requireContext(), R.color.appc_pink)) + this.typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL) + } + } else { + payment_method_fee.visibility = View.GONE + } + } + + private fun loadIcons(paymentMethod: PaymentMethod, view: ImageView?) { + compositeDisposable.add(Observable.fromCallable { + val context = context + GlideApp.with(context!!) + .asBitmap() + .load(paymentMethod.iconUrl) + .submit() + .get() + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { view?.setImageBitmap(it) } + .subscribe({ }) { it.printStackTrace() }) + } + + override fun showError(message: Int) { + if (!itemAlreadyOwnedError) { + payment_method_main_view.visibility = View.GONE + error_message.error_dismiss.text = getString(R.string.ok) + error_message.visibility = View.VISIBLE + error_message.generic_error_layout.error_message.setText(message) + } + } + + override fun showItemAlreadyOwnedError() { + payment_method_main_view.visibility = View.GONE + itemAlreadyOwnedError = true + iabView.disableBack() + error_dismiss.text = getString(R.string.ok) + error_message.visibility = View.VISIBLE + generic_error_layout.error_message.setText( + R.string.purchase_error_incomplete_transaction_body) + layout_support_icn.visibility = View.GONE + layout_support_logo.visibility = View.GONE + contact_us.visibility = View.GONE + } + + override fun finish(bundle: Bundle) { + iabView.finish(bundle) + } + + override fun showPaymentsSkeletonLoading() { + buy_button.isEnabled = false + pre_selected_payment_method_group.visibility = View.GONE + payment_methods_list_group.visibility = View.INVISIBLE + mid_separator?.visibility = View.VISIBLE + payments_skeleton.visibility = View.VISIBLE + } + + override fun showSkeletonLoading() { + showPaymentsSkeletonLoading() + bonus_layout_skeleton.visibility = View.VISIBLE + bonus_msg_skeleton.visibility = View.VISIBLE + } + + override fun showProgressBarLoading() { + payment_methods.visibility = View.INVISIBLE + loading_view.visibility = View.VISIBLE + } + + override fun hideLoading() { + if (processing_loading.visibility != View.VISIBLE) { + payment_methods.visibility = View.VISIBLE + removeSkeletons() + buy_button.isEnabled = true + if (isPreSelected) { + pre_selected_payment_method_group.visibility = View.VISIBLE + payment_methods_list_group.visibility = View.GONE + bottom_separator?.visibility = View.INVISIBLE + layout_pre_selected.visibility = View.VISIBLE + } else { + payment_methods_list_group.visibility = View.VISIBLE + pre_selected_payment_method_group.visibility = View.GONE + } + loading_view.visibility = View.GONE + } + } + + override fun getCancelClick(): Observable { + return RxView.clicks(cancel_button) + } + + override fun getSelectedPaymentMethod(hasPreSelectedPaymentMethod: Boolean): PaymentMethod { + if (!isPreSelected && ::paymentMethodsAdapter.isInitialized.not()) return PaymentMethod() + val checkedButtonId = + if (::paymentMethodsAdapter.isInitialized) paymentMethodsAdapter.getSelectedItem() else -1 + return if (paymentMethodList.isNotEmpty() && !isPreSelected && checkedButtonId != -1) { + paymentMethodList[checkedButtonId] + } else if (hasPreSelectedPaymentMethod && checkedButtonId == -1) { + preSelectedPaymentMethod?.value ?: PaymentMethod() + } else { + PaymentMethod() + } + } + + override fun close(bundle: Bundle) { + iabView.close(bundle) + } + + override fun errorDismisses(): Observable { + return RxView.clicks(error_dismiss) + .map { itemAlreadyOwnedError } + } + + override fun getSupportLogoClicks() = RxView.clicks(layout_support_logo) + + override fun getSupportIconClicks() = RxView.clicks(layout_support_icn) + + override fun showAuthenticationActivity() = iabView.showAuthenticationActivity() + + override fun setupUiCompleted() = setupSubject!! + + override fun showProcessingLoadingDialog() { + payment_methods.visibility = View.INVISIBLE + processing_loading.visibility = View.VISIBLE + } + + override fun getBuyClick(): Observable { + return RxView.clicks(buy_button) + } + + override fun showPaypal(gamificationLevel: Int, fiatValue: FiatValue) { + iabView.showAdyenPayment(fiatValue.amount, fiatValue.currency, isBds, + PaymentType.PAYPAL, bonusMessageValue, false, null, gamificationLevel) + } + + + override fun showAdyen(fiatAmount: BigDecimal, fiatCurrency: String, paymentType: PaymentType, + iconUrl: String?, + gamificationLevel: Int) { + if (!itemAlreadyOwnedError) { + iabView.showAdyenPayment(fiatAmount, fiatCurrency, isBds, paymentType, bonusMessageValue, + true, iconUrl, gamificationLevel) + } + } + + override fun showCreditCard(gamificationLevel: Int, fiatValue: FiatValue) { + iabView.showAdyenPayment(fiatValue.amount, fiatValue.currency, isBds, + PaymentType.CARD, bonusMessageValue, false, null, gamificationLevel) + } + + override fun showAppCoins(gamificationLevel: Int) { + iabView.showOnChain(transactionBuilder!!.amount(), isBds, bonusMessageValue, + gamificationLevel) + } + + override fun showCredits(gamificationLevel: Int) { + iabView.showAppcoinsCreditsPayment(transactionBuilder!!.amount(), gamificationLevel) + } + + override fun showShareLink(selectedPaymentMethod: String) { + val isOneStep: Boolean = transactionBuilder!!.type + .equals("INAPP_UNMANAGED", ignoreCase = true) + iabView.showShareLinkPayment(transactionBuilder!!.domain, transactionBuilder!!.skuId, + if (isOneStep) transactionBuilder!!.originalOneStepValue else null, + if (isOneStep) transactionBuilder!!.originalOneStepCurrency else null, + transactionBuilder!!.amount(), + transactionBuilder!!.type, selectedPaymentMethod) + } + + override fun getPaymentSelection(): Observable { + return Observable.merge(paymentMethodClick + .filter { checkedRadioButtonId -> checkedRadioButtonId >= 0 } + .map { paymentMethodList[it].id }, preSelectedPaymentMethod!!.map( + PaymentMethod::id)) + } + + override fun getMorePaymentMethodsClicks(): Observable { + return RxView.clicks(more_payment_methods) + } + + override fun showLocalPayment(selectedPaymentMethod: String, iconUrl: String, label: String, + gamificationLevel: Int) { + val isOneStep: Boolean = transactionBuilder!!.type + .equals("INAPP_UNMANAGED", ignoreCase = true) + iabView.showLocalPayment(transactionBuilder!!.domain, transactionBuilder!!.skuId, + if (isOneStep) transactionBuilder!!.originalOneStepValue else null, + if (isOneStep) transactionBuilder!!.originalOneStepCurrency else null, bonusMessageValue, + selectedPaymentMethod, transactionBuilder!!.toAddress(), transactionBuilder!!.type, + transactionBuilder!!.amount(), transactionBuilder!!.callbackUrl, + transactionBuilder!!.orderReference, transactionBuilder!!.payload, iconUrl, label, + gamificationLevel) + } + + override fun setBonus(bonus: BigDecimal, currency: String) { + var scaledBonus = bonus.stripTrailingZeros() + .setScale(CurrencyFormatUtils.FIAT_SCALE, BigDecimal.ROUND_DOWN) + var newCurrencyString = currency + if (scaledBonus < BigDecimal("0.01")) { + newCurrencyString = "~$currency" + } + scaledBonus = scaledBonus.max(BigDecimal("0.01")) + val formattedBonus = formatter.formatCurrency(scaledBonus, WalletCurrency.FIAT) + bonusMessageValue = newCurrencyString + formattedBonus + bonus_value.text = getString(R.string.gamification_purchase_header_part_2, bonusMessageValue) + } + + override fun onBackPressed(): Observable { + return iabView.backButtonPress() + .map { itemAlreadyOwnedError } + } + + override fun showNext() = buy_button.setText(R.string.action_next) + + override fun showBuy() = setBuyButtonText() + + private fun setBuyButtonText() { + val buyButtonText = if (isDonation) R.string.action_donate else R.string.action_buy + buy_button.setText(buyButtonText) + } + + override fun showMergedAppcoins(gamificationLevel: Int, fiatValue: FiatValue) { + iabView.showMergedAppcoins(fiatValue.amount, fiatValue.currency, bonusMessageValue, + isBds, isDonation, gamificationLevel) + } + + override fun lockRotation() = iabView.lockRotation() + + override fun showEarnAppcoins() { + iabView.showEarnAppcoins(transactionBuilder!!.domain, transactionBuilder!!.skuId, + transactionBuilder!!.amount(), transactionBuilder!!.type) + } + + override fun showBonus() { + bonus_layout.visibility = View.VISIBLE + bonus_msg.visibility = View.VISIBLE + no_bonus_msg?.visibility = View.INVISIBLE + bottom_separator?.visibility = View.VISIBLE + removeBonusSkeletons() + } + + override fun removeBonus() { + bonus_layout.visibility = View.GONE + bonus_msg.visibility = View.GONE + no_bonus_msg?.visibility = View.GONE + bottom_separator?.visibility = View.GONE + removeBonusSkeletons() + } + + override fun hideBonus() { + bonus_layout.visibility = View.INVISIBLE + bonus_msg.visibility = View.INVISIBLE + bottom_separator?.visibility = View.INVISIBLE + removeBonusSkeletons() + } + + override fun replaceBonus() { + bonus_layout.visibility = View.INVISIBLE + bonus_msg.visibility = View.INVISIBLE + no_bonus_msg?.visibility = View.VISIBLE + removeBonusSkeletons() + } + + override fun onAuthenticationResult(): Observable { + return iabView.onAuthenticationResult() + } + + private fun setupAppNameAndIcon() { + if (isDonation) { + app_sku_description.text = resources.getString(R.string.item_donation) + app_name.text = resources.getString(R.string.item_donation) + } else { + compositeDisposable.add(Single.defer { Single.just(appPackage) } + .observeOn(Schedulers.io()) + .map { packageName -> + Pair(getApplicationName(packageName), + context!!.packageManager.getApplicationIcon(packageName)) + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ setHeaderInfo(it.first, it.second) }) { it.printStackTrace() }) + } + } + + private fun setHeaderInfo(appName: String, appIcon: Drawable) { + app_name?.text = appName + app_icon?.setImageDrawable(appIcon) + app_sku_description.text = productName + } + + private fun getApplicationName(packageName: String): String { + val packageManager = context!!.packageManager + val packageInfo = packageManager.getApplicationInfo(packageName, 0) + return packageManager.getApplicationLabel(packageInfo) + .toString() + } + + private val isBds: Boolean by lazy { + if (arguments!!.containsKey(IS_BDS)) { + arguments!!.getBoolean(IS_BDS) + } else { + throw IllegalArgumentException("isBds data not found") + } + } + + private val isDonation: Boolean by lazy { + if (arguments!!.containsKey(IS_DONATION)) { + arguments!!.getBoolean(IS_DONATION) + } else { + throw IllegalArgumentException("isDonation data not found") + } + } + + private val transactionBuilder: TransactionBuilder? by lazy { + if (arguments!!.containsKey(TRANSACTION)) { + arguments!!.getParcelable(TRANSACTION) as TransactionBuilder? + } else { + throw IllegalArgumentException("transaction data not found") + } + } + + private val productName: String? by lazy { + if (arguments!!.containsKey(IabActivity.PRODUCT_NAME)) { + arguments!!.getString(IabActivity.PRODUCT_NAME) + } else { + throw IllegalArgumentException("productName data not found") + } + } + + private val appPackage: String by lazy { + if (arguments!!.containsKey(IabActivity.APP_PACKAGE)) { + arguments!!.getString(IabActivity.APP_PACKAGE, "") + } else { + throw IllegalArgumentException("appPackage data not found") + } + } + + private fun getDeveloperPayload(): String? { + return if (arguments!!.containsKey(IabActivity.DEVELOPER_PAYLOAD)) { + arguments!!.getString(IabActivity.DEVELOPER_PAYLOAD, "") + } else { + throw IllegalArgumentException("developer payload data not found") + } + } + + private fun getUri(): String? { + return if (arguments!!.containsKey(IabActivity.URI)) { + arguments!!.getString(IabActivity.URI, "") + } else { + throw IllegalArgumentException("uri data not found") + } + } + + private fun getTransactionValue(): BigDecimal { + return if (arguments!!.containsKey(IabActivity.TRANSACTION_AMOUNT)) { + arguments!!.getSerializable(IabActivity.TRANSACTION_AMOUNT) as BigDecimal + } else { + throw java.lang.IllegalArgumentException("transaction value not found") + } + } + + private fun removeBonusSkeletons() { + bonus_layout_skeleton.visibility = View.GONE + bonus_msg_skeleton.visibility = View.GONE + } + + private fun removeSkeletons() { + fiat_price_skeleton.visibility = View.GONE + appc_price_skeleton.visibility = View.GONE + payments_skeleton.visibility = View.GONE + removeBonusSkeletons() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsInteractor.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsInteractor.kt new file mode 100644 index 00000000000..91f5cbeba20 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsInteractor.kt @@ -0,0 +1,110 @@ +package com.asfoundation.wallet.ui.iab + +import android.util.Pair +import com.appcoins.wallet.bdsbilling.Billing +import com.appcoins.wallet.bdsbilling.WalletService +import com.appcoins.wallet.bdsbilling.repository.BillingSupportedType +import com.appcoins.wallet.gamification.repository.ForecastBonusAndLevel +import com.asfoundation.wallet.entity.Balance +import com.asfoundation.wallet.entity.PendingTransaction +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.repository.BdsPendingTransactionService +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.ui.balance.BalanceInteract +import com.asfoundation.wallet.ui.gamification.GamificationInteractor +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import java.math.BigDecimal +import java.util.* + +class PaymentMethodsInteractor(private val walletService: WalletService, + private val supportInteractor: SupportInteractor, + private val gamificationInteractor: GamificationInteractor, + private val balanceInteract: BalanceInteract, + private val walletBlockedInteract: WalletBlockedInteract, + private val inAppPurchaseInteractor: InAppPurchaseInteractor, + private val preferencesRepositoryType: PreferencesRepositoryType, + private val billing: Billing, + private val bdsPendingTransactionService: BdsPendingTransactionService) { + + + fun showSupport(gamificationLevel: Int): Completable { + return walletService.getWalletAddress() + .flatMapCompletable { + Completable.fromAction { + supportInteractor.registerUser(gamificationLevel, it.toLowerCase(Locale.ROOT)) + supportInteractor.displayChatScreen() + } + } + } + + fun getEthBalance(): Observable> = balanceInteract.getEthBalance() + + fun getAppcBalance(): Observable> = balanceInteract.getAppcBalance() + + fun getCreditsBalance(): Observable> = + balanceInteract.getCreditsBalance() + + fun isBonusActiveAndValid() = gamificationInteractor.isBonusActiveAndValid() + + fun isBonusActiveAndValid(forecastBonus: ForecastBonusAndLevel) = + gamificationInteractor.isBonusActiveAndValid(forecastBonus) + + fun getEarningBonus(packageName: String, amount: BigDecimal): Single = + gamificationInteractor.getEarningBonus(packageName, amount) + + fun isWalletBlocked() = walletBlockedInteract.isWalletBlocked() + + fun getCurrentPaymentStep(packageName: String, transactionBuilder: TransactionBuilder) + : Single = + inAppPurchaseInteractor.getCurrentPaymentStep(packageName, transactionBuilder) + + fun resume(uri: String?, transactionType: AsfInAppPurchaseInteractor.TransactionType, + packageName: String, productName: String?, developerPayload: String?, + isBds: Boolean): Completable = + inAppPurchaseInteractor.resume(uri, transactionType, packageName, productName, + developerPayload, isBds) + + fun convertToLocalFiat(appcValue: Double): Single = + inAppPurchaseInteractor.convertToLocalFiat(appcValue) + + fun hasAsyncLocalPayment() = inAppPurchaseInteractor.hasAsyncLocalPayment() + + fun hasPreSelectedPaymentMethod() = inAppPurchaseInteractor.hasPreSelectedPaymentMethod() + + fun removePreSelectedPaymentMethod() = inAppPurchaseInteractor.removePreSelectedPaymentMethod() + + fun removeAsyncLocalPayment() = inAppPurchaseInteractor.removeAsyncLocalPayment() + + fun getPaymentMethods(transaction: TransactionBuilder, transactionValue: String, + currency: String): Single> = + inAppPurchaseInteractor.getPaymentMethods(transaction, transactionValue, currency) + + fun mergeAppcoins(paymentMethods: List): List = + inAppPurchaseInteractor.mergeAppcoins(paymentMethods) + + fun swapDisabledPositions(paymentMethods: List): List = + inAppPurchaseInteractor.swapDisabledPositions(paymentMethods) + + fun getPreSelectedPaymentMethod(): String = inAppPurchaseInteractor.preSelectedPaymentMethod + + fun getLastUsedPaymentMethod(): String = inAppPurchaseInteractor.lastUsedPaymentMethod + + fun hasAuthenticationPermission() = preferencesRepositoryType.hasAuthenticationPermission() + + fun checkTransactionStateFromTransactionId(uid: String): Observable = + bdsPendingTransactionService.checkTransactionStateFromTransactionId(uid) + + fun getSkuTransaction(appPackage: String, skuId: String?, networkThread: Scheduler) = + billing.getSkuTransaction(appPackage, skuId, networkThread) + + fun getSkuPurchase(appPackage: String, skuId: String?, networkThread: Scheduler) = + billing.getSkuPurchase(appPackage, skuId, networkThread) + + fun getPurchases(appPackage: String, inapp: BillingSupportedType, networkThread: Scheduler) = + billing.getPurchases(appPackage, inapp, networkThread) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsMapper.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsMapper.kt new file mode 100644 index 00000000000..d46bc3876c7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsMapper.kt @@ -0,0 +1,43 @@ +package com.asfoundation.wallet.ui.iab + +import android.os.Bundle +import com.appcoins.wallet.bdsbilling.repository.entity.Purchase +import com.appcoins.wallet.billing.BillingMessagesMapper +import com.asfoundation.wallet.ui.iab.PaymentMethodsView.SelectedPaymentMethod + +class PaymentMethodsMapper(private val billingMessagesMapper: BillingMessagesMapper) { + + fun map(paymentId: String): SelectedPaymentMethod { + return when (paymentId) { + "ask_friend" -> SelectedPaymentMethod.SHARE_LINK + "paypal" -> SelectedPaymentMethod.PAYPAL + "credit_card" -> SelectedPaymentMethod.CREDIT_CARD + "appcoins" -> SelectedPaymentMethod.APPC + "appcoins_credits" -> SelectedPaymentMethod.APPC_CREDITS + "merged_appcoins" -> SelectedPaymentMethod.MERGED_APPC + "earn_appcoins" -> SelectedPaymentMethod.EARN_APPC + "" -> SelectedPaymentMethod.ERROR + else -> SelectedPaymentMethod.LOCAL_PAYMENTS + } + } + + fun map(selectedPaymentMethod: SelectedPaymentMethod): String { + return when (selectedPaymentMethod) { + SelectedPaymentMethod.SHARE_LINK -> "ask_friend" + SelectedPaymentMethod.PAYPAL -> "paypal" + SelectedPaymentMethod.CREDIT_CARD -> "credit_card" + SelectedPaymentMethod.APPC -> "appcoins" + SelectedPaymentMethod.APPC_CREDITS -> "appcoins_credits" + SelectedPaymentMethod.MERGED_APPC -> "merged_appcoins" + SelectedPaymentMethod.LOCAL_PAYMENTS -> "local_payments" + SelectedPaymentMethod.EARN_APPC -> "earn_appcoins" + SelectedPaymentMethod.ERROR -> "" + } + } + + fun mapCancellation() = billingMessagesMapper.mapCancellation() + + fun mapFinishedPurchase(purchase: Purchase, itemAlreadyOwned: Boolean): Bundle { + return billingMessagesMapper.mapFinishedPurchase(purchase, itemAlreadyOwned) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsPresenter.kt new file mode 100644 index 00000000000..b494a61602f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsPresenter.kt @@ -0,0 +1,653 @@ +package com.asfoundation.wallet.ui.iab + +import android.os.Bundle +import com.appcoins.wallet.bdsbilling.repository.BillingSupportedType +import com.appcoins.wallet.bdsbilling.repository.entity.Purchase +import com.appcoins.wallet.bdsbilling.repository.entity.Transaction +import com.appcoins.wallet.gamification.repository.ForecastBonusAndLevel +import com.asf.wallet.R +import com.asfoundation.wallet.billing.adyen.PaymentType +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.ui.PaymentNavigationData +import com.asfoundation.wallet.ui.iab.PaymentMethodsView.SelectedPaymentMethod +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.asfoundation.wallet.util.isNoNetworkException +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.Function3 +import retrofit2.HttpException +import java.util.* +import java.util.concurrent.TimeUnit + +class PaymentMethodsPresenter( + private val view: PaymentMethodsView, + private val viewScheduler: Scheduler, + private val networkThread: Scheduler, + private val disposables: CompositeDisposable, + private val analytics: PaymentMethodsAnalytics, + private val transaction: TransactionBuilder, + private val paymentMethodsMapper: PaymentMethodsMapper, + private val formatter: CurrencyFormatUtils, + private val logger: Logger, + private val interactor: PaymentMethodsInteractor, + private val paymentMethodsData: PaymentMethodsData) { + + private var cachedGamificationLevel = 0 + private var cachedFiatValue: FiatValue? = null + private var cachedPaymentNavigationData: PaymentNavigationData? = null + private var hasStartedAuth = false + + companion object { + private val TAG = PaymentMethodsPresenter::class.java.name + private const val GAMIFICATION_LEVEL = "gamification_level" + private const val HAS_STARTED_AUTH = "has_started_auth" + private const val FIAT_VALUE = "fiat_value" + private const val PAYMENT_NAVIGATION_DATA = "payment_navigation_data" + } + + fun present(savedInstanceState: Bundle?) { + savedInstanceState?.let { + cachedGamificationLevel = savedInstanceState.getInt(GAMIFICATION_LEVEL) + hasStartedAuth = savedInstanceState.getBoolean(HAS_STARTED_AUTH) + cachedFiatValue = savedInstanceState.getSerializable(FIAT_VALUE) as FiatValue? + cachedPaymentNavigationData = + savedInstanceState.getSerializable(PAYMENT_NAVIGATION_DATA) as PaymentNavigationData? + } + handleOnGoingPurchases() + handleCancelClick() + handleErrorDismisses() + handleMorePaymentMethodClicks() + handleBuyClick() + handleSupportClicks() + handleAuthenticationResult() + if (paymentMethodsData.isBds) handlePaymentSelection() + } + + fun onResume(firstRun: Boolean) { + if (firstRun.not()) view.showPaymentsSkeletonLoading() + setupUi(firstRun) + } + + private fun handlePaymentSelection() { + disposables.add(view.getPaymentSelection() + .observeOn(viewScheduler) + .doOnNext { selectedPaymentMethod -> + if (interactor.isBonusActiveAndValid()) { + handleBonusVisibility(selectedPaymentMethod) + } + handlePositiveButtonText(selectedPaymentMethod) + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleBuyClick() { + disposables.add(view.getBuyClick() + .map { view.getSelectedPaymentMethod(interactor.hasPreSelectedPaymentMethod()) } + .observeOn(viewScheduler) + .doOnNext { handleBuyAnalytics(it) } + .doOnNext { selectedPaymentMethod -> + when (paymentMethodsMapper.map(selectedPaymentMethod.id)) { + SelectedPaymentMethod.EARN_APPC -> view.showEarnAppcoins() + SelectedPaymentMethod.APPC_CREDITS -> { + view.showProgressBarLoading() + handleWalletBlockStatus(selectedPaymentMethod) + } + SelectedPaymentMethod.MERGED_APPC -> view.showMergedAppcoins(cachedGamificationLevel, + cachedFiatValue!!) + + else -> { + if (interactor.hasAuthenticationPermission()) { + showAuthenticationActivity(selectedPaymentMethod, + interactor.hasPreSelectedPaymentMethod()) + } else { + when (paymentMethodsMapper.map(selectedPaymentMethod.id)) { + SelectedPaymentMethod.PAYPAL -> view.showPaypal(cachedGamificationLevel, + cachedFiatValue!!) + SelectedPaymentMethod.CREDIT_CARD -> view.showCreditCard(cachedGamificationLevel, + cachedFiatValue!!) + SelectedPaymentMethod.APPC -> view.showAppCoins(cachedGamificationLevel) + SelectedPaymentMethod.SHARE_LINK -> view.showShareLink(selectedPaymentMethod.id) + SelectedPaymentMethod.LOCAL_PAYMENTS -> view.showLocalPayment( + selectedPaymentMethod.id, selectedPaymentMethod.iconUrl, + selectedPaymentMethod.label, cachedGamificationLevel) + else -> return@doOnNext + } + } + } + } + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleAuthenticationResult() { + disposables.add(view.onAuthenticationResult() + .observeOn(viewScheduler) + .doOnNext { + if (cachedPaymentNavigationData == null) close() + else if (!it) { + hasStartedAuth = false + if (cachedPaymentNavigationData!!.isPreselected && paymentMethodsMapper.map( + cachedPaymentNavigationData!!.paymentId) == SelectedPaymentMethod.CREDIT_CARD) { + close() + } + } else { + navigateToPayment(cachedPaymentNavigationData!!) + } + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleWalletBlockStatus(selectedPaymentMethod: PaymentMethod) { + disposables.add(interactor.isWalletBlocked() + .subscribeOn(networkThread) + .observeOn(viewScheduler) + .doOnSuccess { + if (interactor.hasAuthenticationPermission()) { + showAuthenticationActivity(selectedPaymentMethod, + interactor.hasPreSelectedPaymentMethod()) + } else { + view.showCredits(cachedGamificationLevel) + } + } + .doOnError { showError(it) } + .subscribe({}, { showError(it) }) + ) + } + + private fun handleOnGoingPurchases() { + if (transaction.skuId == null) { + disposables.add(isSetupCompleted() + .doOnComplete { view.hideLoading() } + .subscribeOn(viewScheduler) + .subscribe({}, { it.printStackTrace() })) + return + } + disposables.add(waitForUi(transaction.skuId) + .observeOn(viewScheduler) + .doOnComplete { view.hideLoading() } + .subscribe({ }, { showError(it) })) + } + + private fun navigateToPayment(paymentNavigationData: PaymentNavigationData) { + when (paymentMethodsMapper.map(paymentNavigationData.paymentId)) { + SelectedPaymentMethod.PAYPAL -> view.showPaypal(cachedGamificationLevel, cachedFiatValue!!) + SelectedPaymentMethod.CREDIT_CARD -> { + if (paymentNavigationData.isPreselected) { + view.showAdyen(cachedFiatValue!!.amount, cachedFiatValue!!.currency, PaymentType.CARD, + paymentNavigationData.paymentIconUrl, cachedGamificationLevel) + } else view.showCreditCard(cachedGamificationLevel, cachedFiatValue!!) + } + SelectedPaymentMethod.APPC -> view.showAppCoins(cachedGamificationLevel) + SelectedPaymentMethod.APPC_CREDITS -> view.showCredits(cachedGamificationLevel) + SelectedPaymentMethod.SHARE_LINK -> view.showShareLink(paymentNavigationData.paymentId) + SelectedPaymentMethod.LOCAL_PAYMENTS -> { + view.showLocalPayment(paymentNavigationData.paymentId, paymentNavigationData.paymentIconUrl, + paymentNavigationData.paymentLabel, cachedGamificationLevel) + } + else -> { + view.showError(R.string.unknown_error) + logger.log(TAG, "Wrong payment method after authentication.") + } + } + } + + private fun isSetupCompleted(): Completable { + return view.setupUiCompleted() + .takeWhile { isViewSet -> !isViewSet } + .ignoreElements() + } + + private fun waitForUi(skuId: String?): Completable { + return Completable.mergeArray(checkProcessing(skuId), checkAndConsumePrevious(skuId), + isSetupCompleted()) + } + + private fun checkProcessing(skuId: String?): Completable { + return interactor.getSkuTransaction(paymentMethodsData.appPackage, skuId, + networkThread) + .subscribeOn(networkThread) + .filter { (_, status) -> status === Transaction.Status.PROCESSING } + .observeOn(viewScheduler) + .doOnSuccess { view.showProcessingLoadingDialog() } + .doOnSuccess { handleProcessing() } + .map { it.uid } + .observeOn(networkThread) + .flatMapCompletable { uid -> + interactor.checkTransactionStateFromTransactionId(uid) + .ignoreElements() + .andThen(finishProcess(skuId)) + } + } + + private fun handleProcessing() { + disposables.add( + interactor.getCurrentPaymentStep(paymentMethodsData.appPackage, transaction) + .filter { currentPaymentStep -> currentPaymentStep == AsfInAppPurchaseInteractor.CurrentPaymentStep.PAUSED_ON_CHAIN } + .doOnSuccess { + view.lockRotation() + interactor.resume(paymentMethodsData.uri, + AsfInAppPurchaseInteractor.TransactionType.NORMAL, + paymentMethodsData.appPackage, transaction.skuId, + paymentMethodsData.developerPayload, paymentMethodsData.isBds) + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun finishProcess(skuId: String?): Completable { + return interactor.getSkuPurchase(paymentMethodsData.appPackage, skuId, + networkThread) + .observeOn(viewScheduler) + .doOnSuccess { purchase -> finish(purchase, false) } + .ignoreElement() + } + + private fun checkAndConsumePrevious(sku: String?): Completable { + return getPurchases() + .subscribeOn(networkThread) + .observeOn(viewScheduler) + .flatMapCompletable { purchases -> + Completable.fromAction { + if (hasRequestedSkuPurchase(purchases, sku)) view.showItemAlreadyOwnedError() + } + } + } + + private fun setupUi(firstRun: Boolean) { + disposables.add( + interactor.convertToLocalFiat(paymentMethodsData.transactionValue.toDouble()) + .subscribeOn(networkThread) + .flatMapCompletable { fiatValue -> + this.cachedFiatValue = fiatValue + getPaymentMethods(fiatValue) + .flatMapCompletable { paymentMethods -> + interactor.getEarningBonus(transaction.domain, transaction.amount()) + .observeOn(viewScheduler) + .flatMapCompletable { + Completable.fromAction { + setupBonusInformation(it) + selectPaymentMethod(paymentMethods, fiatValue, + interactor.isBonusActiveAndValid(it)) + } + } + } + } + .subscribeOn(networkThread) + .observeOn(viewScheduler) + .doOnComplete { + //If first run we should rely on the hideLoading of the handleOnGoingPurchases method + if (!firstRun) view.hideLoading() + } + .subscribe({ }, { this.showError(it) })) + } + + private fun setupBonusInformation(forecastBonus: ForecastBonusAndLevel) { + if (interactor.isBonusActiveAndValid(forecastBonus)) { + view.setBonus(forecastBonus.amount, forecastBonus.currency) + } else { + view.removeBonus() + } + cachedGamificationLevel = forecastBonus.level + analytics.setGamificationLevel(cachedGamificationLevel) + } + + private fun selectPaymentMethod(paymentMethods: List, fiatValue: FiatValue, + isBonusActive: Boolean) { + val fiatAmount = formatter.formatCurrency(fiatValue.amount, WalletCurrency.FIAT) + val appcAmount = formatter.formatCurrency(transaction.amount(), WalletCurrency.APPCOINS) + if (interactor.hasAsyncLocalPayment()) { + //After a asynchronous payment credits will be used as pre selected + getCreditsPaymentMethod(paymentMethods)?.let { + if (it.isEnabled) { + showPreSelectedPaymentMethod(fiatValue, it, fiatAmount, appcAmount, isBonusActive) + return + } + } + } + + if (interactor.hasPreSelectedPaymentMethod()) { + val paymentMethod = getPreSelectedPaymentMethod(paymentMethods) + if (paymentMethod == null || !paymentMethod.isEnabled) { + showPaymentMethods(fiatValue, paymentMethods, + PaymentMethodsView.PaymentMethodId.CREDIT_CARD.id, fiatAmount, appcAmount) + } else { + when (paymentMethod.id) { + PaymentMethodsView.PaymentMethodId.CREDIT_CARD.id -> { + analytics.sendPurchaseDetailsEvent(paymentMethodsData.appPackage, transaction.skuId, + transaction.amount() + .toString(), transaction.type) + if (interactor.hasAuthenticationPermission()) { + if (!hasStartedAuth) { + showAuthenticationActivity(paymentMethod, true) + hasStartedAuth = true + } + } else { + view.showAdyen(fiatValue.amount, fiatValue.currency, PaymentType.CARD, + paymentMethod.iconUrl, cachedGamificationLevel) + } + } + else -> showPreSelectedPaymentMethod(fiatValue, paymentMethod, fiatAmount, appcAmount, + isBonusActive) + } + } + } else { + val paymentMethodId = getLastUsedPaymentMethod(paymentMethods) + showPaymentMethods(fiatValue, paymentMethods, paymentMethodId, fiatAmount, appcAmount) + } + } + + private fun getCreditsPaymentMethod(paymentMethods: List): PaymentMethod? { + paymentMethods.forEach { + if (it.id == PaymentMethodsView.PaymentMethodId.MERGED_APPC.id) { + val mergedPaymentMethod = it as AppCoinsPaymentMethod + return PaymentMethod(PaymentMethodsView.PaymentMethodId.APPC_CREDITS.id, + mergedPaymentMethod.creditsLabel, mergedPaymentMethod.iconUrl, mergedPaymentMethod.fee, + mergedPaymentMethod.isCreditsEnabled) + } + if (it.id == PaymentMethodsView.PaymentMethodId.APPC_CREDITS.id) { + return it + } + } + + return null + } + + private fun showPaymentMethods(fiatValue: FiatValue, paymentMethods: List, + paymentMethodId: String, fiatAmount: String, appcAmount: String) { + var appcEnabled = false + var creditsEnabled = false + val paymentList: MutableList + val symbol = mapCurrencyCodeToSymbol(fiatValue.currency) + if (paymentMethodsData.isBds) { + paymentMethods.forEach { + if (it is AppCoinsPaymentMethod) { + appcEnabled = it.isAppcEnabled + creditsEnabled = it.isCreditsEnabled + } + } + paymentList = paymentMethods.toMutableList() + } else { + paymentList = paymentMethods + .filter { + it.id == paymentMethodsMapper.map(SelectedPaymentMethod.APPC) + } + .toMutableList() + } + view.showPaymentMethods(paymentList, symbol, paymentMethodId, fiatAmount, appcAmount, + appcEnabled, creditsEnabled) + sendPaymentMethodsEvents() + } + + private fun showPreSelectedPaymentMethod(fiatValue: FiatValue, paymentMethod: PaymentMethod, + fiatAmount: String, appcAmount: String, + isBonusActive: Boolean) { + view.showPreSelectedPaymentMethod(paymentMethod, mapCurrencyCodeToSymbol(fiatValue.currency), + fiatAmount, appcAmount, isBonusActive) + sendPreSelectedPaymentMethodsEvents() + } + + private fun mapCurrencyCodeToSymbol(currencyCode: String): String { + return if (currencyCode.equals("APPC", ignoreCase = true)) + currencyCode + else + Currency.getInstance(currencyCode).currencyCode + } + + private fun handleCancelClick() { + disposables.add(view.getCancelClick() + .map { view.getSelectedPaymentMethod(interactor.hasPreSelectedPaymentMethod()) } + .observeOn(networkThread) + .doOnNext { sendCancelPaymentMethodAnalytics(it) } + .subscribe { close() }) + } + + private fun sendCancelPaymentMethodAnalytics(paymentMethod: PaymentMethod) { + analytics.sendPaymentMethodEvent(paymentMethodsData.appPackage, transaction.skuId, + transaction.amount() + .toString(), paymentMethod.id, transaction.type, "cancel", + interactor.hasPreSelectedPaymentMethod()) + + } + + private fun handleMorePaymentMethodClicks() { + disposables.add(view.getMorePaymentMethodsClicks() + .map { view.getSelectedPaymentMethod(interactor.hasPreSelectedPaymentMethod()) } + .observeOn(networkThread) + .doOnNext { selectedPaymentMethod -> + analytics.sendPaymentMethodEvent(paymentMethodsData.appPackage, + transaction.skuId, + transaction.amount() + .toString(), selectedPaymentMethod.id, transaction.type, "other_payments") + } + .observeOn(viewScheduler) + .doOnEach { view.showSkeletonLoading() } + .flatMapSingle { + if (cachedFiatValue == null) { + interactor.convertToLocalFiat(paymentMethodsData.transactionValue.toDouble()) + .subscribeOn(networkThread) + } else { + Single.just(cachedFiatValue) + } + } + .flatMapCompletable { fiatValue -> + getPaymentMethods(fiatValue).subscribeOn(networkThread) + .observeOn(viewScheduler) + .flatMapCompletable { paymentMethods -> + Completable.fromAction { + val fiatAmount = formatter.formatCurrency(fiatValue.amount, WalletCurrency.FIAT) + val appcAmount = formatter.formatCurrency(transaction.amount(), + WalletCurrency.APPCOINS) + val paymentMethodId = getLastUsedPaymentMethod(paymentMethods) + showPaymentMethods(fiatValue, paymentMethods, paymentMethodId, fiatAmount, + appcAmount) + } + } + .andThen( + Completable.fromAction { interactor.removePreSelectedPaymentMethod() }) + .andThen(Completable.fromAction { interactor.removeAsyncLocalPayment() }) + .andThen(Completable.fromAction { view.hideLoading() }) + } + .subscribe({ }, { this.showError(it) })) + } + + private fun showError(t: Throwable) { + t.printStackTrace() + logger.log(TAG, t) + when { + t.isNoNetworkException() -> view.showError(R.string.notification_no_network_poa) + isItemAlreadyOwnedError(t) -> view.showItemAlreadyOwnedError() + else -> view.showError(R.string.activity_iab_error_message) + } + } + + private fun isItemAlreadyOwnedError(throwable: Throwable): Boolean { + return throwable is HttpException && throwable.code() == 409 + } + + private fun close() { + view.close(paymentMethodsMapper.mapCancellation()) + } + + private fun handleErrorDismisses() { + disposables.add(Observable.merge(view.errorDismisses(), view.onBackPressed()) + .flatMapCompletable { itemAlreadyOwned -> + if (itemAlreadyOwned) { + getPurchases().doOnSuccess { purchases -> + val purchase = getRequestedSkuPurchase(purchases, transaction.skuId) + purchase?.let { finish(it, itemAlreadyOwned) } ?: view.close(Bundle()) + } + .ignoreElement() + } else { + return@flatMapCompletable Completable.fromAction { view.close(Bundle()) } + } + } + .subscribe({ }, { view.close(Bundle()) })) + } + + private fun handleSupportClicks() { + disposables.add(Observable.merge(view.getSupportIconClicks(), view.getSupportLogoClicks()) + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .flatMapCompletable { interactor.showSupport(cachedGamificationLevel) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun finish(purchase: Purchase, itemAlreadyOwned: Boolean) { + view.finish(paymentMethodsMapper.mapFinishedPurchase(purchase, itemAlreadyOwned)) + } + + private fun sendPaymentMethodsEvents() { + analytics.sendPurchaseDetailsEvent(paymentMethodsData.appPackage, transaction.skuId, + transaction.amount() + .toString(), transaction.type) + } + + private fun sendPreSelectedPaymentMethodsEvents() { + analytics.sendPurchaseDetailsEvent(paymentMethodsData.appPackage, transaction.skuId, + transaction.amount() + .toString(), transaction.type) + } + + fun stop() = disposables.clear() + + + private fun getPaymentMethods(fiatValue: FiatValue): Single> { + return if (paymentMethodsData.isBds) { + interactor.getPaymentMethods(transaction, fiatValue.amount.toString(), + fiatValue.currency) + .map { interactor.mergeAppcoins(it) } + .map { interactor.swapDisabledPositions(it) } + .doOnSuccess { updateBalanceDao() } + } else { + Single.just(listOf(PaymentMethod.APPC)) + } + } + + //Updates database with the latest balance to take less time loading the merged appcoins view + private fun updateBalanceDao() { + disposables.add( + Observable.zip(interactor.getEthBalance(), + interactor.getCreditsBalance(), + interactor.getAppcBalance(), Function3 { _: Any, _: Any, _: Any -> }) + .take(1) + .subscribeOn(networkThread) + .subscribe({}, { it.printStackTrace() })) + } + + private fun getPreSelectedPaymentMethod(paymentMethods: List): PaymentMethod? { + val preSelectedPreference = interactor.getPreSelectedPaymentMethod() + for (paymentMethod in paymentMethods) { + if (paymentMethod.id == PaymentMethodsView.PaymentMethodId.MERGED_APPC.id) { + if (preSelectedPreference == PaymentMethodsView.PaymentMethodId.APPC.id) { + val mergedPaymentMethod = paymentMethod as AppCoinsPaymentMethod + return PaymentMethod(PaymentMethodsView.PaymentMethodId.APPC.id, + mergedPaymentMethod.appcLabel, mergedPaymentMethod.iconUrl, mergedPaymentMethod.fee, + mergedPaymentMethod.isAppcEnabled) + } + if (preSelectedPreference == PaymentMethodsView.PaymentMethodId.APPC_CREDITS.id) { + val mergedPaymentMethod = paymentMethod as AppCoinsPaymentMethod + return PaymentMethod(PaymentMethodsView.PaymentMethodId.APPC_CREDITS.id, + mergedPaymentMethod.creditsLabel, paymentMethod.creditsIconUrl, + mergedPaymentMethod.fee, mergedPaymentMethod.isCreditsEnabled) + } + } + if (paymentMethod.id == preSelectedPreference) { + return paymentMethod + } + } + return null + } + + private fun getLastUsedPaymentMethod(paymentMethods: List): String { + val lastUsedPaymentMethod = interactor.getLastUsedPaymentMethod() + for (it in paymentMethods) { + if (it.isEnabled) { + if (it.id == PaymentMethodsView.PaymentMethodId.MERGED_APPC.id && + (lastUsedPaymentMethod == PaymentMethodsView.PaymentMethodId.APPC.id || + lastUsedPaymentMethod == PaymentMethodsView.PaymentMethodId.APPC_CREDITS.id)) { + return PaymentMethodsView.PaymentMethodId.MERGED_APPC.id + } + if (it.id == lastUsedPaymentMethod) { + return it.id + } + } + } + return PaymentMethodsView.PaymentMethodId.CREDIT_CARD.id + } + + private fun handleBonusVisibility(selectedPaymentMethod: String) { + when (selectedPaymentMethod) { + paymentMethodsMapper.map(SelectedPaymentMethod.EARN_APPC) -> view.replaceBonus() + paymentMethodsMapper.map(SelectedPaymentMethod.MERGED_APPC) -> view.hideBonus() + paymentMethodsMapper.map(SelectedPaymentMethod.APPC_CREDITS) -> view.hideBonus() + else -> view.showBonus() + } + } + + private fun handlePositiveButtonText(selectedPaymentMethod: String) { + if (selectedPaymentMethod == paymentMethodsMapper.map( + SelectedPaymentMethod.MERGED_APPC) || selectedPaymentMethod == paymentMethodsMapper.map( + SelectedPaymentMethod.EARN_APPC)) { + view.showNext() + } else { + view.showBuy() + } + } + + private fun handleBuyAnalytics(selectedPaymentMethod: PaymentMethod) { + val action = + if (selectedPaymentMethod.id == PaymentMethodsView.PaymentMethodId.MERGED_APPC.id) "next" else "buy" + if (interactor.hasPreSelectedPaymentMethod()) { + analytics.sendPaymentMethodEvent(paymentMethodsData.appPackage, transaction.skuId, + transaction.amount() + .toString(), selectedPaymentMethod.id, transaction.type, action) + } else { + analytics.sendPaymentMethodEvent(paymentMethodsData.appPackage, transaction.skuId, + transaction.amount() + .toString(), selectedPaymentMethod.id, transaction.type, action) + } + } + + private fun getPurchases(): Single> { + return interactor.getPurchases(paymentMethodsData.appPackage, + BillingSupportedType.INAPP, + networkThread) + } + + private fun hasRequestedSkuPurchase(purchases: List, sku: String?): Boolean { + for (purchase in purchases) { + if (purchase.product.name == sku) { + return true + } + } + return false + } + + private fun getRequestedSkuPurchase(purchases: List, sku: String?): Purchase? { + for (purchase in purchases) { + if (purchase.product.name == sku) { + return purchase + } + } + return null + } + + private fun showAuthenticationActivity(paymentMethod: PaymentMethod, isPreselected: Boolean) { + cachedPaymentNavigationData = + PaymentNavigationData(paymentMethod.id, paymentMethod.label, paymentMethod.iconUrl, + isPreselected) + view.showAuthenticationActivity() + } + + fun onSavedInstance(outState: Bundle) { + outState.putInt(GAMIFICATION_LEVEL, cachedGamificationLevel) + outState.putBoolean(HAS_STARTED_AUTH, hasStartedAuth) + outState.putSerializable(FIAT_VALUE, cachedFiatValue) + outState.putSerializable(PAYMENT_NAVIGATION_DATA, cachedPaymentNavigationData) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsView.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsView.kt new file mode 100644 index 00000000000..aca0d1bd917 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsView.kt @@ -0,0 +1,110 @@ +package com.asfoundation.wallet.ui.iab + +import android.os.Bundle +import com.asfoundation.wallet.billing.adyen.PaymentType +import io.reactivex.Observable +import java.math.BigDecimal + +interface PaymentMethodsView { + fun showPaymentMethods(paymentMethods: MutableList, + currency: String, paymentMethodId: String, fiatAmount: String, + appcAmount: String, appcEnabled: Boolean, creditsEnabled: Boolean) + + fun showPreSelectedPaymentMethod(paymentMethod: PaymentMethod, currency: String, + fiatAmount: String, appcAmount: String, isBonusActive: Boolean) + + fun showError(message: Int) + + fun showItemAlreadyOwnedError() + + fun finish(bundle: Bundle) + + fun showPaymentsSkeletonLoading() + + fun showSkeletonLoading() + + fun showProgressBarLoading() + + fun hideLoading() + + fun getCancelClick(): Observable + + fun close(bundle: Bundle) + + fun errorDismisses(): Observable + + fun setupUiCompleted(): Observable + + fun showProcessingLoadingDialog() + + fun getBuyClick(): Observable + + fun showPaypal(gamificationLevel: Int, fiatValue: FiatValue) + + fun showAdyen(fiatAmount: BigDecimal, + fiatCurrency: String, + paymentType: PaymentType, + iconUrl: String?, gamificationLevel: Int) + + fun showCreditCard(gamificationLevel: Int, fiatValue: FiatValue) + + fun showAppCoins(gamificationLevel: Int) + + fun showCredits(gamificationLevel: Int) + + fun showShareLink(selectedPaymentMethod: String) + + fun getPaymentSelection(): Observable + + fun getMorePaymentMethodsClicks(): Observable + + fun showLocalPayment(selectedPaymentMethod: String, iconUrl: String, label: String, + gamificationLevel: Int) + + fun setBonus(bonus: BigDecimal, currency: String) + + fun onBackPressed(): Observable + + fun showNext() + + fun showBuy() + + fun showMergedAppcoins(gamificationLevel: Int, fiatValue: FiatValue) + + fun lockRotation() + + fun showEarnAppcoins() + + fun showBonus() + + fun hideBonus() + + fun replaceBonus() + + fun removeBonus() + + fun getSupportLogoClicks(): Observable + + fun getSupportIconClicks(): Observable + + fun showAuthenticationActivity() + + fun onAuthenticationResult(): Observable + + fun getSelectedPaymentMethod(hasPreSelectedPaymentMethod: Boolean): PaymentMethod + + enum class SelectedPaymentMethod { + PAYPAL, CREDIT_CARD, APPC, APPC_CREDITS, MERGED_APPC, SHARE_LINK, LOCAL_PAYMENTS, EARN_APPC, + ERROR + } + + enum class PaymentMethodId(val id: String) { + PAYPAL("paypal"), + APPC("appcoins"), + APPC_CREDITS("appcoins_credits"), + MERGED_APPC("merged_appcoins"), + CREDIT_CARD("credit_card"), + ASK_FRIEND("ask_friend") + + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsViewHolder.kt new file mode 100644 index 00000000000..c34b40b9d4e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/PaymentMethodsViewHolder.kt @@ -0,0 +1,113 @@ +package com.asfoundation.wallet.ui.iab + +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Typeface +import android.view.View +import android.widget.ImageView +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R +import com.asfoundation.wallet.GlideApp +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import kotlinx.android.synthetic.main.item_payment_method.view.* + +class PaymentMethodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + fun bind(data: PaymentMethod, checked: Boolean, listener: View.OnClickListener) { + GlideApp.with(itemView.context) + .load(data.iconUrl) + .into(itemView.payment_method_ic) + + val selected = data.isEnabled && checked + itemView.radio_button.isChecked = selected + itemView.radio_button.isEnabled = data.isEnabled + + handleDescription(data, selected) + handleFee(data.fee, data.isEnabled) + + if (data.isEnabled) { + itemView.setOnClickListener(listener) + itemView.radio_button.visibility = View.VISIBLE + hideDisableReason() + removeAlphaScale(itemView.payment_method_ic) + } else { + itemView.setOnClickListener(null) + itemView.radio_button.visibility = View.INVISIBLE + itemView.background = null + if (data.disabledReason != null) { + showDisableReason(data.disabledReason) + } else { + hideDisableReason() + } + + applyAlphaScale(itemView.payment_method_ic) + } + } + + private fun handleDescription(data: PaymentMethod, selected: Boolean) { + itemView.payment_method_description.text = data.label + if (selected) { + itemView.payment_method_description.setTextColor( + ContextCompat.getColor(itemView.context, R.color.details_address_text_color)) + itemView.payment_method_description.typeface = + Typeface.create("sans-serif-medium", Typeface.NORMAL) + } else { + itemView.payment_method_description.setTextColor( + ContextCompat.getColor(itemView.context, R.color.grey_alpha_active_54)) + itemView.payment_method_description.typeface = Typeface.create("sans-serif", Typeface.NORMAL) + } + } + + private fun handleFee(fee: PaymentMethodFee?, enabled: Boolean) { + if (fee?.isValidFee() == true) { + itemView.payment_method_fee.visibility = View.VISIBLE + val formattedValue = CurrencyFormatUtils.create() + .formatCurrency(fee.amount!!, WalletCurrency.FIAT) + itemView.payment_method_fee_value.text = "$formattedValue ${fee.currency}" + + itemView.payment_method_fee_value.apply { + if (enabled) { + this.setTextColor(ContextCompat.getColor(itemView.context, R.color.appc_pink)) + this.typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL) + } else { + this.setTextColor(ContextCompat.getColor(itemView.context, R.color.grey_alpha_active_54)) + this.typeface = Typeface.create("sans-serif", Typeface.NORMAL) + } + } + + } else { + itemView.payment_method_fee.visibility = View.GONE + } + } + + private fun applyAlphaScale(imageView: ImageView) { + val colorMatrix = ColorMatrix() + colorMatrix.setSaturation(0f) + val filter = ColorMatrixColorFilter(colorMatrix) + imageView.colorFilter = filter + } + + private fun removeAlphaScale(imageView: ImageView) { + val colorMatrix = ColorMatrix() + colorMatrix.setSaturation(1f) + val filter = ColorMatrixColorFilter(colorMatrix) + imageView.colorFilter = filter + } + + private fun showDisableReason(@StringRes reason: Int?) { + reason?.let { + itemView.payment_method_reason.visibility = View.VISIBLE + itemView.payment_method_reason.text = itemView.context.getString(it) + } + } + + private fun hideDisableReason() { + itemView.payment_method_reason.visibility = View.GONE + itemView.payment_method_reason.text = null + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/RewardPayment.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/RewardPayment.kt new file mode 100644 index 00000000000..c64c8fde550 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/RewardPayment.kt @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.ui.iab + +data class RewardPayment(val orderReference: String?, + val status: Status, val errorCode: Int? = null, + val errorMessage: String? = null) + +enum class Status { + PROCESSING, COMPLETED, ERROR, FORBIDDEN, NO_NETWORK +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/RewardsManager.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/RewardsManager.kt new file mode 100644 index 00000000000..ce2fb1d0612 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/RewardsManager.kt @@ -0,0 +1,71 @@ +package com.asfoundation.wallet.ui.iab + +import com.appcoins.wallet.appcoins.rewards.AppcoinsRewards +import com.appcoins.wallet.appcoins.rewards.AppcoinsRewardsRepository +import com.appcoins.wallet.appcoins.rewards.Transaction +import com.appcoins.wallet.bdsbilling.Billing +import com.appcoins.wallet.bdsbilling.repository.entity.Purchase +import com.asfoundation.wallet.billing.partners.AddressService +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.functions.BiFunction +import io.reactivex.schedulers.Schedulers +import java.math.BigDecimal + +class RewardsManager(private val appcoinsRewards: AppcoinsRewards, private val billing: Billing, + private val partnerAddressService: AddressService) { + + val balance: Single + get() = appcoinsRewards.getBalance() + + fun pay(sku: String?, amount: BigDecimal, developerAddress: String, packageName: String, + origin: String?, type: String, payload: String?, callbackUrl: String?, + orderReference: String?, referrerUrl: String?): Completable { + return Single.zip(partnerAddressService.getStoreAddressForPackage(packageName), + partnerAddressService.getOemAddressForPackage(packageName), + BiFunction { storeAddress: String, oemAddress: String -> Pair(storeAddress, oemAddress) }) + .flatMapCompletable { + appcoinsRewards.pay(amount, origin, sku, type, developerAddress, it.first, it.second, + packageName, payload, callbackUrl, orderReference, referrerUrl) + } + } + + fun getPaymentCompleted(packageName: String, sku: String?): Single { + return billing.getSkuPurchase(packageName, sku, Schedulers.io()) + } + + fun getTransaction(packageName: String, sku: String?, + amount: BigDecimal): Observable { + return appcoinsRewards.getPayment(packageName, sku, amount.toString()) + } + + fun getPaymentStatus(packageName: String, sku: String?, + amount: BigDecimal): Observable { + return appcoinsRewards.getPayment(packageName, sku, amount.toString()) + .flatMap { this.map(it) } + } + + private fun map(transaction: Transaction): Observable { + return when (transaction.status) { + Transaction.Status.PROCESSING -> Observable.just( + RewardPayment(transaction.orderReference, Status.PROCESSING)) + Transaction.Status.COMPLETED -> Observable.just( + RewardPayment(transaction.orderReference, Status.COMPLETED)) + Transaction.Status.ERROR -> Observable.just( + RewardPayment(transaction.orderReference, Status.ERROR, transaction.errorCode, + transaction.errorMessage)) + Transaction.Status.FORBIDDEN -> Observable.just( + RewardPayment(transaction.orderReference, Status.FORBIDDEN)) + Transaction.Status.NO_NETWORK -> Observable.just( + RewardPayment(transaction.orderReference, Status.NO_NETWORK)) + else -> throw UnsupportedOperationException( + "Transaction status " + transaction.status + " not supported") + } + } + + fun sendCredits(toWallet: String, amount: BigDecimal, + packageName: String): Single { + return appcoinsRewards.sendCredits(toWallet, amount, packageName) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/TranslatablePaymentMethods.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/TranslatablePaymentMethods.kt new file mode 100644 index 00000000000..3760fac5b6e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/TranslatablePaymentMethods.kt @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.ui.iab + +import com.asf.wallet.R + +enum class TranslatablePaymentMethods(val paymentMethod: String, val stringId: Int) { + ASK_SOMEONE_PAY("ask_friend", R.string.askafriend_payment_option_button), + CREDIT_CARD("credit_card", R.string.dialog_bank_card), + EARN_APPCOINS("earn_appcoins", R.string.purchase_poa_item) +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/WebViewActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/WebViewActivity.kt new file mode 100644 index 00000000000..bc91f397359 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/WebViewActivity.kt @@ -0,0 +1,88 @@ +package com.asfoundation.wallet.ui.iab + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.res.AssetManager +import android.content.res.Configuration +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.Surface +import androidx.appcompat.app.AppCompatActivity +import com.asf.wallet.R +import dagger.android.AndroidInjection + +class WebViewActivity : AppCompatActivity() { + + override fun getAssets(): AssetManager { + //Workaround for crash when inflating the webView + return if (Build.VERSION.SDK_INT > 22) { + super.getAssets() + } else { + resources.assets + } + } + + private lateinit var billingWebViewFragment: BillingWebViewFragment + + public override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.web_view_activity) + lockCurrentPosition() + + if (savedInstanceState == null) { + val url = intent.getStringExtra(URL) + billingWebViewFragment = BillingWebViewFragment.newInstance(url) + supportFragmentManager.beginTransaction() + .add(R.id.container, billingWebViewFragment) + .commit() + } + } + + override fun onBackPressed() { + if (!((this::billingWebViewFragment.isInitialized) && billingWebViewFragment.handleBackPressed())) { + super.onBackPressed() + } + } + + @SuppressLint("SourceLockedOrientationActivity") + private fun lockCurrentPosition() { + //setRequestedOrientation requires translucent and floating to be false to work in API 26 + val rotation = windowManager.defaultDisplay + .rotation + val orientation = resources.configuration.orientation + + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } + } else if (rotation == Surface.ROTATION_180 || rotation == Surface.ROTATION_270) { + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + } + } else { + Log.w("WebView", "Invalid orientation value: $orientation") + } + + } + + companion object { + + const val SUCCESS = 1 + const val FAIL = 0 + private const val URL = "url" + + fun newIntent(activity: Activity?, url: String?): Intent { + return Intent(activity, WebViewActivity::class.java).apply { + putExtra(URL, url) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/database/AppCoinsOperationDao.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/database/AppCoinsOperationDao.java index ecdf6f856a8..02f05479134 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/iab/database/AppCoinsOperationDao.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/database/AppCoinsOperationDao.java @@ -1,9 +1,9 @@ package com.asfoundation.wallet.ui.iab.database; -import android.arch.persistence.room.Dao; -import android.arch.persistence.room.Delete; -import android.arch.persistence.room.Insert; -import android.arch.persistence.room.Query; +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.Query; import io.reactivex.Flowable; import java.util.List; diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/database/AppCoinsOperationDatabase.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/database/AppCoinsOperationDatabase.java index 13a3cac4697..970bc7d62a7 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/iab/database/AppCoinsOperationDatabase.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/database/AppCoinsOperationDatabase.java @@ -1,7 +1,7 @@ package com.asfoundation.wallet.ui.iab.database; -import android.arch.persistence.room.Database; -import android.arch.persistence.room.RoomDatabase; +import androidx.room.Database; +import androidx.room.RoomDatabase; @Database(entities = AppCoinsOperationEntity.class, version = 1) public abstract class AppCoinsOperationDatabase extends RoomDatabase { diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/database/AppCoinsOperationEntity.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/database/AppCoinsOperationEntity.java index db46af780ba..677f85b7039 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/iab/database/AppCoinsOperationEntity.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/database/AppCoinsOperationEntity.java @@ -1,9 +1,9 @@ package com.asfoundation.wallet.ui.iab.database; -import android.arch.persistence.room.ColumnInfo; -import android.arch.persistence.room.Entity; -import android.arch.persistence.room.PrimaryKey; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.PrimaryKey; @Entity public class AppCoinsOperationEntity { @NonNull @PrimaryKey @ColumnInfo(name = "key") private final String key; diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/AtomicBigInteger.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/AtomicBigInteger.java new file mode 100644 index 00000000000..c965e6f9579 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/AtomicBigInteger.java @@ -0,0 +1,37 @@ +package com.asfoundation.wallet.ui.iab.raiden; + +import java.math.BigInteger; +import java.util.concurrent.atomic.AtomicReference; + +public final class AtomicBigInteger { + + private final AtomicReference valueHolder = new AtomicReference<>(); + + public AtomicBigInteger(BigInteger bigInteger) { + valueHolder.set(bigInteger); + } + + public BigInteger getAndIncrement() { + for (; ; ) { + BigInteger current = valueHolder.get(); + BigInteger next = current.add(BigInteger.ONE); + if (valueHolder.compareAndSet(current, next)) { + return current; + } + } + } + + public BigInteger get() { + return valueHolder.get(); + } + + public void increment() { + for (; ; ) { + BigInteger current = valueHolder.get(); + BigInteger next = current.add(BigInteger.ONE); + if (valueHolder.compareAndSet(current, next)) { + return; + } + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/MultiWalletNonceObtainer.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/MultiWalletNonceObtainer.java new file mode 100644 index 00000000000..e23975b7877 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/MultiWalletNonceObtainer.java @@ -0,0 +1,60 @@ +package com.asfoundation.wallet.ui.iab.raiden; + +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; +import org.web3j.abi.datatypes.Address; + +public class MultiWalletNonceObtainer { + private static final String TAG = MultiWalletNonceObtainer.class.getSimpleName(); + private final Map nonceObtainers; + private final NonceObtainerFactory nonceObtainerFactory; + + public MultiWalletNonceObtainer(NonceObtainerFactory nonceObtainerFactory) { + this.nonceObtainers = new HashMap<>(); + this.nonceObtainerFactory = nonceObtainerFactory; + } + + public BigInteger getNonce(Address address, long chainId) { + return getNonceObtainer(address, chainId).getNonce(); + } + + public boolean consumeNonce(BigInteger nonce, Address address, long chainId) { + return getNonceObtainer(address, chainId).consumeNonce(nonce); + } + + private NonceObtainer getNonceObtainer(Address address, long chainId) { + NonceObtainer nonceObtainer = nonceObtainers.get(new Key(address, chainId)); + if (nonceObtainer == null) { + nonceObtainer = nonceObtainerFactory.build(address); + nonceObtainers.put(new Key(address, chainId), nonceObtainer); + } + return nonceObtainer; + } + + private static class Key { + private final Address address; + private final long chainId; + + private Key(Address address, long chainId) { + this.address = address; + this.chainId = chainId; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Key)) return false; + + Key key = (Key) o; + + if (chainId != key.chainId) return false; + return address.equals(key.address); + } + + @Override public int hashCode() { + int result = address.hashCode(); + result = 31 * result + (int) (chainId ^ (chainId >>> 32)); + return result; + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/NonceObtainer.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/NonceObtainer.java new file mode 100644 index 00000000000..32c773d1695 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/NonceObtainer.java @@ -0,0 +1,64 @@ +package com.asfoundation.wallet.ui.iab.raiden; + +import java.io.IOException; +import java.math.BigInteger; +import org.web3j.abi.datatypes.Address; + +public class NonceObtainer { + private final int refreshIntervalMillis; + private final NonceProvider nonceProvider; + private final Address address; + private final Object object = new Object(); + private AtomicBigInteger atomicBigInteger; + private long refreshTime; + + /** + * @param refreshIntervalMillis time window between each nonce sync with ethereum network. + * @param address + */ + public NonceObtainer(int refreshIntervalMillis, NonceProvider nonceProvider, Address address) { + this.refreshIntervalMillis = refreshIntervalMillis; + this.nonceProvider = nonceProvider; + this.address = address; + } + + public BigInteger getNonce() { + synchronized (object) { + if (atomicBigInteger == null + || System.currentTimeMillis() - refreshTime > refreshIntervalMillis) { + refresh(); + } + return atomicBigInteger.get(); + } + } + + public boolean consumeNonce(BigInteger nonce) { + synchronized (object) { + if (atomicBigInteger == null) { + throw new IllegalStateException("No nonce was get for the wallet " + address.toString()); + } + if (atomicBigInteger.get() + .compareTo(nonce) == 0) { + atomicBigInteger.increment(); + return true; + } else { + return false; + } + } + } + + private void refresh() { + try { + refreshTime = System.currentTimeMillis(); + BigInteger count = nonceProvider.getNonce(address); + if (atomicBigInteger == null + || atomicBigInteger.get() + .compareTo(count) < 0) { + atomicBigInteger = new AtomicBigInteger(count); + } + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/NonceObtainerFactory.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/NonceObtainerFactory.java new file mode 100644 index 00000000000..f64b46434e5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/NonceObtainerFactory.java @@ -0,0 +1,17 @@ +package com.asfoundation.wallet.ui.iab.raiden; + +import org.web3j.abi.datatypes.Address; + +public class NonceObtainerFactory { + private final int refreshIntervalMillis; + private final NonceProvider nonceProvider; + + public NonceObtainerFactory(int refreshIntervalMillis, NonceProvider nonceProvider) { + this.refreshIntervalMillis = refreshIntervalMillis; + this.nonceProvider = nonceProvider; + } + + public NonceObtainer build(Address address) { + return new NonceObtainer(refreshIntervalMillis, nonceProvider, address); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/NonceProvider.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/NonceProvider.java new file mode 100644 index 00000000000..dcd9a6690a1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/NonceProvider.java @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.ui.iab.raiden; + +import java.io.IOException; +import java.math.BigInteger; +import org.web3j.abi.datatypes.Address; + +public interface NonceProvider { + BigInteger getNonce(Address address) throws IOException; +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/RaidenRepository.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/RaidenRepository.java new file mode 100644 index 00000000000..b87577e1539 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/RaidenRepository.java @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.ui.iab.raiden; + +public interface RaidenRepository { + boolean shouldShowDialog(); + + void setShouldShowDialog(boolean shouldShow); +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/Web3jNonceProvider.java b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/Web3jNonceProvider.java new file mode 100644 index 00000000000..27720abdc70 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/raiden/Web3jNonceProvider.java @@ -0,0 +1,22 @@ +package com.asfoundation.wallet.ui.iab.raiden; + +import com.asfoundation.wallet.repository.Web3jProvider; +import java.io.IOException; +import java.math.BigInteger; +import org.web3j.abi.datatypes.Address; +import org.web3j.protocol.core.DefaultBlockParameterName; + +public class Web3jNonceProvider implements NonceProvider { + private final Web3jProvider web3jProvider; + + public Web3jNonceProvider(Web3jProvider web3jProvider) { + this.web3jProvider = web3jProvider; + } + + @Override public BigInteger getNonce(Address address) throws IOException { + return web3jProvider.getDefault() + .ethGetTransactionCount(address.toString(), DefaultBlockParameterName.PENDING) + .send() + .getTransactionCount(); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/share/ShareLinkInteractor.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/share/ShareLinkInteractor.kt new file mode 100644 index 00000000000..c64ba00506f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/share/ShareLinkInteractor.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.ui.iab.share + +import com.asfoundation.wallet.billing.share.ShareLinkRepository +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.ui.iab.InAppPurchaseInteractor +import io.reactivex.Single + +class ShareLinkInteractor(private val remoteRepository: ShareLinkRepository, + private val walletInteractor: FindDefaultWalletInteract, + private val inAppPurchaseInteractor: InAppPurchaseInteractor) { + + fun getLinkToShare(domain: String, skuId: String?, message: String?, + originalAmount: String?, originalCurrency: String?, + paymentMethod: String): Single { + return walletInteractor.find() + .flatMap { + remoteRepository.getLink(domain, skuId, message, it.address, originalAmount, + originalCurrency, paymentMethod) + } + } + + fun savePreSelectedPaymentMethod(paymentMethod: String) { + inAppPurchaseInteractor.savePreSelectedPaymentMethod(paymentMethod) + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/share/SharePaymentLinkFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/share/SharePaymentLinkFragment.kt new file mode 100644 index 00000000000..474766304bc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/share/SharePaymentLinkFragment.kt @@ -0,0 +1,222 @@ +package com.asfoundation.wallet.ui.iab.share + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.app.ShareCompat +import androidx.core.content.res.ResourcesCompat +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.ui.iab.IabView +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.fragment_share_payment_link.* +import java.math.BigDecimal +import javax.inject.Inject + +class SharePaymentLinkFragment : BasePageViewFragment(), + SharePaymentLinkFragmentView { + + @Inject + lateinit var interactor: ShareLinkInteractor + + lateinit var presenter: SharePaymentLinkPresenter + private var iabView: IabView? = null + + @Inject + lateinit var analytics: BillingAnalytics + + companion object { + + private const val PARAM_DOMAIN = "AMOUNT_DOMAIN" + private const val PARAM_SKUID = "AMOUNT_SKUID" + private const val PARAM_ORIGINAL_AMOUNT = "PARAM_ORIGINAL_AMOUNT" + private const val PARAM_AMOUNT = "PARAM_AMOUNT" + private const val PARAM_ORIGINAL_CURRENCY = "PARAM_ORIGINAL_CURRENCY" + private const val PARAM_TRANSACTION_TYPE = "PARAM_TRANSACTION_TYPE" + private const val PAYMENT_METHOD_NAME = "ASK_SOMEONE" + private const val PARAM_PAYMENT_KEY = "PAYMENT_NAME" + + @JvmStatic + fun newInstance(domain: String, skuId: String?, originalAmount: String?, + originalCurrency: String?, amount: BigDecimal, + type: String, paymentMethod: String): SharePaymentLinkFragment = + SharePaymentLinkFragment().apply { + arguments = Bundle(2).apply { + putString(PARAM_DOMAIN, domain) + putString(PARAM_SKUID, skuId) + putString(PARAM_ORIGINAL_AMOUNT, originalAmount) + putString(PARAM_ORIGINAL_CURRENCY, originalCurrency) + putString(PARAM_TRANSACTION_TYPE, type) + putString(PARAM_PAYMENT_KEY, paymentMethod) + putSerializable(PARAM_AMOUNT, amount) + } + } + } + + val domain: String by lazy { + if (arguments!!.containsKey(PARAM_DOMAIN)) { + arguments!!.getString(PARAM_DOMAIN)!! + } else { + throw IllegalArgumentException("Domain not found") + } + } + + val paymentMethod: String by lazy { + if (arguments!!.containsKey(PARAM_PAYMENT_KEY)) { + arguments!!.getString(PARAM_PAYMENT_KEY)!! + } else { + throw IllegalArgumentException("paymentMethod not found") + } + } + + val type: String by lazy { + if (arguments!!.containsKey(PARAM_TRANSACTION_TYPE)) { + arguments!!.getString(PARAM_TRANSACTION_TYPE)!! + } else { + throw IllegalArgumentException("type not found") + } + } + + private val originalAmount: String? by lazy { + if (arguments!!.containsKey( + PARAM_ORIGINAL_AMOUNT)) { + arguments!!.getString( + PARAM_ORIGINAL_AMOUNT) + } else { + throw IllegalArgumentException("Original amount not found") + } + } + + private val originalCurrency: String? by lazy { + if (arguments!!.containsKey( + PARAM_ORIGINAL_CURRENCY)) { + arguments!!.getString( + PARAM_ORIGINAL_CURRENCY) + } else { + throw IllegalArgumentException("Domain not found") + } + } + + val skuId: String? by lazy { + if (arguments!!.containsKey( + PARAM_SKUID)) { + val value = arguments!!.getString( + PARAM_SKUID) ?: return@lazy null + value + } else { + throw IllegalArgumentException("SkuId not found") + } + } + + val amount: BigDecimal by lazy { + if (arguments!!.containsKey( + PARAM_AMOUNT)) { + val value = arguments!!.getSerializable( + PARAM_AMOUNT) as BigDecimal + value + } else { + throw IllegalArgumentException("amount not found") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null) { + analytics.sendPaymentEvent(domain, skuId, amount.toString(), PAYMENT_METHOD_NAME, type) + } + presenter = + SharePaymentLinkPresenter(this, interactor, AndroidSchedulers.mainThread(), Schedulers.io(), + CompositeDisposable(), analytics) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_share_payment_link, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present() + } + + override fun onAttach(context: Context) { + if (context !is IabView) { + throw IllegalStateException("share payment link fragment must be attached to IAB activity") + } + iabView = context + super.onAttach(context) + } + + override fun onDetach() { + iabView = null + super.onDetach() + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + override fun getShareButtonClick(): Observable { + return RxView.clicks(share_btn) + .map { + val message = if (note.text.isNotEmpty()) note.text.toString() else null + SharePaymentLinkFragmentView.SharePaymentData(domain, skuId, message, originalAmount, + originalCurrency, paymentMethod, amount.toFloat() + .toString(), type) + } + } + + override fun getCancelButtonClick(): Observable { + return RxView.clicks(close_btn) + .map { + val message = if (note.text.isNotEmpty()) note.text.toString() else null + SharePaymentLinkFragmentView.SharePaymentData(domain, skuId, message, originalAmount, + originalCurrency, paymentMethod, amount.toFloat() + .toString(), type) + } + } + + override fun showFetchingLinkInfo() { + share_link_title.text = getString(R.string.askafriend_generating_link_message) + share_link_title.setTextColor( + ResourcesCompat.getColor(resources, R.color.share_link_title_color, null)) + close_btn.visibility = View.INVISIBLE + share_btn.visibility = View.INVISIBLE + } + + override fun showErrorInfo() { + share_link_title.text = getString(R.string.askafriend_generating_link_error_message) + share_link_title.setTextColor( + ResourcesCompat.getColor(resources, R.color.share_link_error_text_color, null)) + close_btn.visibility = View.VISIBLE + share_btn.visibility = View.VISIBLE + } + + override fun shareLink(url: String) { + share_link_title.text = getString(R.string.askafriend_share_body) + share_link_title.setTextColor( + ResourcesCompat.getColor(resources, R.color.share_link_title_color, null)) + close_btn.visibility = View.VISIBLE + share_btn.visibility = View.VISIBLE + + activity?.let { + ShareCompat.IntentBuilder.from(it) + .setText(url) + .setType("text/plain") + .setChooserTitle(R.string.askafriend_share_popup_title) + .startChooser() + } + } + + override fun close() { + iabView?.close(Bundle()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/share/SharePaymentLinkFragmentView.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/share/SharePaymentLinkFragmentView.kt new file mode 100644 index 00000000000..24ef60fe644 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/share/SharePaymentLinkFragmentView.kt @@ -0,0 +1,24 @@ +package com.asfoundation.wallet.ui.iab.share + +import io.reactivex.Observable + +interface SharePaymentLinkFragmentView { + + fun getShareButtonClick(): Observable + + fun getCancelButtonClick(): Observable + + fun shareLink(url: String) + + fun showFetchingLinkInfo() + + fun showErrorInfo() + + fun close() + + data class SharePaymentData(val domain: String, val skuId: String?, + val message: String?, + val originalAmount: String?, + val originalCurrency: String?, val paymentMethod: String, + val amount: String, val type: String) +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/iab/share/SharePaymentLinkPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/iab/share/SharePaymentLinkPresenter.kt new file mode 100644 index 00000000000..e8276f1c9c2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/iab/share/SharePaymentLinkPresenter.kt @@ -0,0 +1,66 @@ +package com.asfoundation.wallet.ui.iab.share + +import com.asfoundation.wallet.billing.analytics.BillingAnalytics +import com.asfoundation.wallet.ui.iab.PaymentMethodsView +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.BiFunction +import java.util.concurrent.TimeUnit + +class SharePaymentLinkPresenter(private val view: SharePaymentLinkFragmentView, + private val interactor: ShareLinkInteractor, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val analytics: BillingAnalytics) { + + + fun present() { + handleStop() + handleShare() + } + + private fun handleShare() { + disposables.add(view.getShareButtonClick() + .doOnNext { view.showFetchingLinkInfo() } + .flatMapSingle { + analytics.sendPaymentConfirmationEvent(it.domain, it.skuId ?: "", it.amount, + it.paymentMethod, it.type, "share") + getLink(it) + } + .observeOn(viewScheduler) + .doOnNext { + interactor.savePreSelectedPaymentMethod(PaymentMethodsView.PaymentMethodId.ASK_FRIEND.id) + view.shareLink(it) + } + .subscribe({}, { + it.printStackTrace() + view.showErrorInfo() + })) + } + + private fun handleStop() { + disposables.add(view.getCancelButtonClick() + .doOnNext { + analytics.sendPaymentConfirmationEvent(it.domain, it.skuId ?: "", it.amount, + it.paymentMethod, it.type, "close") + view.close() + } + .subscribe()) + } + + fun stop() { + disposables.clear() + } + + private fun getLink(data: SharePaymentLinkFragmentView.SharePaymentData): Single { + return Single.zip( + Single.timer(1, TimeUnit.SECONDS), + interactor.getLinkToShare(data.domain, data.skuId, data.message, data.originalAmount, + data.originalCurrency, data.paymentMethod) + .subscribeOn(networkScheduler), + BiFunction { _: Long, url: String -> url }) + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingActivity.kt new file mode 100644 index 00000000000..be2cd407114 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingActivity.kt @@ -0,0 +1,284 @@ +package com.asfoundation.wallet.ui.onboarding + +import android.animation.Animator +import android.content.Intent +import android.graphics.Typeface +import android.net.Uri +import android.os.Bundle +import android.text.SpannableString +import android.text.Spanned +import android.text.TextPaint +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.text.style.StyleSpan +import android.view.View +import android.view.animation.AnimationUtils +import android.widget.TextView +import com.asf.wallet.R +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.router.ExternalBrowserRouter +import com.asfoundation.wallet.router.TransactionsRouter +import com.asfoundation.wallet.ui.BaseActivity +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import com.asfoundation.wallet.wallet_validation.generic.WalletValidationActivity +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.AndroidInjection +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.ReplaySubject +import kotlinx.android.synthetic.main.activity_onboarding.* +import kotlinx.android.synthetic.main.layout_validation_no_internet.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class OnboardingActivity : BaseActivity(), OnboardingView { + + private lateinit var listener: OnboardingPageChangeListener + + @Inject + lateinit var interactor: OnboardingInteract + + @Inject + lateinit var logger: Logger + + private lateinit var browserRouter: ExternalBrowserRouter + private lateinit var presenter: OnboardingPresenter + private lateinit var adapter: OnboardingPageAdapter + private var linkSubject: PublishSubject? = null + private var paymentMethodsIcons: ArrayList? = null + + companion object { + fun newInstance() = OnboardingActivity() + + private const val TERMS_CONDITIONS_URL = "https://catappult.io/appcoins-wallet/terms-conditions" + private const val PRIVACY_POLICY_URL = "https://catappult.io/appcoins-wallet/privacy-policy" + private const val PAYMENT_METHODS_ICONS = "paymentMethodsIcons" + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + paymentMethodsIcons?.let { outState.putStringArrayList(PAYMENT_METHODS_ICONS, it) } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + AndroidInjection.inject(this) + setContentView(R.layout.activity_onboarding) + browserRouter = ExternalBrowserRouter() + linkSubject = PublishSubject.create() + presenter = OnboardingPresenter(CompositeDisposable(), this, interactor, + AndroidSchedulers.mainThread(), Schedulers.io(), ReplaySubject.create(), logger) + setupUI(savedInstanceState) + + presenter.present() + } + + override fun onResume() { + super.onResume() + sendPageViewEvent() + } + + override fun onDestroy() { + linkSubject = null + presenter.stop() + create_wallet_animation.removeAllAnimatorListeners() + create_wallet_animation.removeAllUpdateListeners() + create_wallet_animation.removeAllLottieOnCompositionLoadedListener() + super.onDestroy() + } + + private fun setupUI(savedInstanceState: Bundle?) { + val termsConditions = resources.getString(R.string.terms_and_conditions) + val privacyPolicy = resources.getString(R.string.privacy_policy) + val termsPolicyTickBox = + resources.getString(R.string.terms_and_conditions_tickbox, termsConditions, + privacyPolicy) + + val spannableString = SpannableString(termsPolicyTickBox) + setLinkToString(spannableString, termsConditions, TERMS_CONDITIONS_URL) + setLinkToString(spannableString, privacyPolicy, PRIVACY_POLICY_URL) + + terms_conditions_body.text = spannableString + terms_conditions_body.isClickable = true + terms_conditions_body.movementMethod = LinkMovementMethod.getInstance() + + adapter = OnboardingPageAdapter(createDefaultItemList()) + + onboarding_viewpager.setPageTransformer(OnboardingPageTransformer()) + onboarding_viewpager.adapter = adapter + val paymentMethodsIcons = + if (savedInstanceState != null && savedInstanceState.containsKey(PAYMENT_METHODS_ICONS)) { + savedInstanceState.getStringArrayList(PAYMENT_METHODS_ICONS)!! + .toList() + } else { + emptyList() + } + listener = + OnboardingPageChangeListener(onboarding_content, paymentMethodsIcons = paymentMethodsIcons) + onboarding_viewpager.registerOnPageChangeCallback(listener) + + onboarding_content.visibility = View.VISIBLE + wallet_creation_animation.visibility = View.GONE + onboarding_layout_validation_no_internet.visibility = View.GONE + } + + override fun updateUI(maxAmount: String, isActive: Boolean) { + if (isActive) { + listener.setIsActiveFlag(isActive) + adapter.setPages(createReferralsItemList(maxAmount)) + } else { + adapter.setPages(createDefaultItemList()) + } + listener.updateUI() + } + + override fun getNextButtonClick() = RxView.clicks(next_button) + + override fun getRedeemButtonClick() = RxView.clicks(been_invited_bonus) + + override fun getSkipClicks() = RxView.clicks(skip_button) + + override fun showViewPagerLastPage() { + onboarding_viewpager.setCurrentItem(onboarding_viewpager.adapter?.itemCount ?: 0, true) + } + + override fun setPaymentMethodsIcons(paymentMethodsIcons: List) { + this.paymentMethodsIcons = ArrayList(paymentMethodsIcons) + listener.setPaymentMethodsIcons(paymentMethodsIcons) + } + + override fun getLinkClick() = linkSubject!! + + override fun showWarningText() { + if (!onboarding_checkbox.isChecked && + terms_conditions_warning.visibility == View.INVISIBLE && + terms_conditions_layout.visibility == View.VISIBLE) { + animateShowWarning(terms_conditions_warning) + terms_conditions_warning.visibility = View.VISIBLE + presenter.markedWarningTextAsShowed() + } + } + + private fun animateShowWarning(textView: TextView) { + val animation = AnimationUtils.loadAnimation(applicationContext, R.anim.fast_fade_in_animation) + textView.animation = animation + } + + override fun showLoading() { + onboarding_content.visibility = View.GONE + wallet_creation_animation.visibility = View.VISIBLE + create_wallet_animation.setAnimation(R.raw.create_wallet_loading_animation) + create_wallet_animation.playAnimation() + } + + private fun navigate(walletValidationStatus: WalletValidationStatus?) { + if (walletValidationStatus == null || walletValidationStatus == WalletValidationStatus.SUCCESS) { + TransactionsRouter().open(this, true) + } else { + val intent = WalletValidationActivity.newIntent(this, hasBeenInvitedFlow = true, + navigateToTransactionsOnSuccess = true, navigateToTransactionsOnCancel = true, + showToolbar = false, previousContext = "onboarding") + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + } + presenter.markOnboardingCompleted() + } + + override fun finishOnboarding(walletValidationStatus: WalletValidationStatus, + showAnimation: Boolean) { + if (!showAnimation) { + navigate(walletValidationStatus) + finish() + return + } + create_wallet_animation.setAnimation(R.raw.success_animation) + create_wallet_text.text = getText(R.string.provide_wallet_created_header) + create_wallet_animation.addAnimatorListener(object : Animator.AnimatorListener { + override fun onAnimationRepeat(animation: Animator?) = Unit + + override fun onAnimationEnd(animation: Animator?) { + navigate(walletValidationStatus) + finish() + } + + override fun onAnimationCancel(animation: Animator?) = Unit + + override fun onAnimationStart(animation: Animator?) = Unit + }) + create_wallet_animation.repeatCount = 0 + create_wallet_animation.playAnimation() + } + + private fun setLinkToString(spannableString: SpannableString, highlightString: String, + uri: String) { + val clickableSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + linkSubject?.onNext(uri) + } + + override fun updateDrawState(ds: TextPaint) { + ds.color = resources.getColor(R.color.grey_alpha_active_54) + ds.isUnderlineText = true + } + } + val indexHighlightString = spannableString.toString() + .indexOf(highlightString) + val highlightStringLength = highlightString.length + spannableString.setSpan(clickableSpan, indexHighlightString, + indexHighlightString + highlightStringLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannableString.setSpan(StyleSpan(Typeface.BOLD), indexHighlightString, + indexHighlightString + highlightStringLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + + override fun navigateToBrowser(uri: Uri) = browserRouter.open(this, uri) + + override fun showNoInternetView() { + stopRetryAnimation() + onboarding_content.visibility = View.GONE + wallet_creation_animation.visibility = View.GONE + onboarding_layout_validation_no_internet.visibility = View.VISIBLE + } + + override fun getRetryButtonClicks(): Observable { + return RxView.clicks(retry_button) + .doOnNext { playRetryAnimation() } + .delay(1, TimeUnit.SECONDS) + } + + override fun getLaterButtonClicks() = RxView.clicks(later_button) + + private fun playRetryAnimation() { + retry_button.visibility = View.GONE + later_button.visibility = View.GONE + retry_animation.visibility = View.VISIBLE + retry_animation.playAnimation() + } + + private fun stopRetryAnimation() { + retry_button.visibility = View.VISIBLE + later_button.visibility = View.VISIBLE + retry_animation.visibility = View.GONE + } + + private fun createReferralsItemList(maxAmount: String): List { + val item1 = OnboardingItem(R.string.intro_1_title, this.getString(R.string.intro_1_body)) + val item2 = OnboardingItem(R.string.intro_2_title, this.getString(R.string.intro_2_body)) + val item3 = OnboardingItem(R.string.intro_3_title, this.getString(R.string.intro_3_body)) + val item4 = OnboardingItem(R.string.referral_onboarding_title, + this.getString(R.string.referral_onboarding_body, maxAmount)) + return listOf(item1, item2, item3, item4) + } + + private fun createDefaultItemList(): List { + val item1 = OnboardingItem(R.string.intro_1_title, this.getString(R.string.intro_1_body)) + val item2 = OnboardingItem(R.string.intro_2_title, this.getString(R.string.intro_2_body)) + val item3 = OnboardingItem(R.string.intro_3_title, this.getString(R.string.intro_3_body)) + val item4 = OnboardingItem(R.string.intro_5_title, + this.getString(R.string.intro_5_body)) + return listOf(item1, item2, item3, item4) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingInteract.kt b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingInteract.kt new file mode 100644 index 00000000000..84fb210fa03 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingInteract.kt @@ -0,0 +1,53 @@ +package com.asfoundation.wallet.ui.onboarding + +import com.appcoins.wallet.bdsbilling.WalletService +import com.appcoins.wallet.bdsbilling.repository.BdsRepository +import com.appcoins.wallet.bdsbilling.repository.entity.PaymentMethodEntity +import com.appcoins.wallet.gamification.Gamification +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.referrals.ReferralInteractorContract +import com.asfoundation.wallet.referrals.ReferralModel +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import io.reactivex.Single + +class OnboardingInteract( + private val walletService: WalletService, + private val preferencesRepositoryType: PreferencesRepositoryType, + private val supportInteractor: SupportInteractor, + private val gamificationRepository: Gamification, + private val smsValidationInteract: SmsValidationInteract, + private val referralInteractor: ReferralInteractorContract, + private val bdsRepository: BdsRepository) { + + fun getWalletAddress() = walletService.getWalletOrCreate() + .flatMap { address -> + gamificationRepository.getUserStats(address) + .doOnSuccess { supportInteractor.registerUser(it.level, address) } + .map { address } + } + + fun finishOnboarding() = preferencesRepositoryType.setOnboardingComplete() + + fun clickSkipOnboarding() = preferencesRepositoryType.setOnboardingSkipClicked() + + fun hasClickedSkipOnboarding() = preferencesRepositoryType.hasClickedSkipOnboarding() + + fun hasOnboardingCompleted() = preferencesRepositoryType.hasCompletedOnboarding() + + fun isAddressValid(address: String): Single = + smsValidationInteract.getValidationStatus(address) + + fun getReferralInfo(): Single = referralInteractor.getReferralInfo() + + fun getPaymentMethodsIcons(): Single> { + return bdsRepository.getPaymentMethods(currencyType = "fiat", direct = true) + .map { map(it) } + .onErrorReturn { emptyList() } + } + + private fun map(paymentMethodEntity: List): List { + return paymentMethodEntity.map { it.iconUrl } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingItem.kt b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingItem.kt new file mode 100644 index 00000000000..3eaaf6de908 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingItem.kt @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.ui.onboarding + +import androidx.annotation.StringRes + +data class OnboardingItem( + @StringRes val title: Int, + val message: String +) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPageAdapter.kt b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPageAdapter.kt new file mode 100644 index 00000000000..0017f335e3e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPageAdapter.kt @@ -0,0 +1,28 @@ +package com.asfoundation.wallet.ui.onboarding + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R + +class OnboardingPageAdapter(private var items: List) : + RecyclerView.Adapter() { + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(container: ViewGroup, viewType: Int): OnboardingViewHolder { + val view = LayoutInflater.from(container.context) + .inflate(R.layout.layout_page_intro, container, false) + return OnboardingViewHolder(view) + } + + override fun onBindViewHolder(holder: OnboardingViewHolder, position: Int) { + holder.bind(items[position]) + } + + fun setPages(items: List) { + this.items = items + notifyDataSetChanged() + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPageChangeListener.kt b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPageChangeListener.kt new file mode 100644 index 00000000000..e954f0c26f3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPageChangeListener.kt @@ -0,0 +1,143 @@ +package com.asfoundation.wallet.ui.onboarding + +import android.view.View +import android.view.animation.AnimationUtils +import android.widget.Button +import android.widget.CheckBox +import android.widget.LinearLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 +import com.airbnb.lottie.LottieAnimationView +import com.asf.wallet.R +import com.rd.PageIndicatorView + + +class OnboardingPageChangeListener internal constructor(private val view: View, + private var isActive: Boolean = false, + private var paymentMethodsIcons: List = emptyList()) : + ViewPager2.OnPageChangeCallback() { + + companion object { + private const val ANIMATION_TRANSITIONS = 3 + private const val PAGE_COUNT = 4 + } + + private var lottieViewPortrait: LottieAnimationView? = null + private var lottieViewLandscape: LottieAnimationView? = null + private lateinit var skipButton: Button + private lateinit var nextButton: Button + private lateinit var beenInvitedButton: Button + private lateinit var paymentMethodsRecyclerView: RecyclerView + private lateinit var checkBox: CheckBox + private lateinit var warningText: TextView + private lateinit var termsConditionsLayout: LinearLayout + private lateinit var pageIndicatorView: PageIndicatorView + private var currentPage = 0 + + init { + init() + } + + fun init() { + lottieViewPortrait = view.findViewById(R.id.lottie_onboarding_portrait) + lottieViewLandscape = view.findViewById(R.id.lottie_onboarding_landscape) + skipButton = view.findViewById(R.id.skip_button) + nextButton = view.findViewById(R.id.next_button) + checkBox = view.findViewById(R.id.onboarding_checkbox) + beenInvitedButton = view.findViewById(R.id.been_invited_bonus) + paymentMethodsRecyclerView = view.findViewById(R.id.payment_methods_recycler_view) + warningText = view.findViewById(R.id.terms_conditions_warning) + termsConditionsLayout = view.findViewById(R.id.terms_conditions_layout) + pageIndicatorView = view.findViewById(R.id.page_indicator) + updatePageIndicator(0) + handleUI(0) + } + + fun setIsActiveFlag(isActive: Boolean) { + this.isActive = isActive + } + + fun updateUI() { + if (isActive && currentPage == 3) beenInvitedButton.visibility = View.VISIBLE + } + + fun setPaymentMethodsIcons(paymentMethodsIcons: List) { + this.paymentMethodsIcons = paymentMethodsIcons + paymentMethodsRecyclerView.adapter = OnboardingPaymentMethodAdapter(paymentMethodsIcons) + if (currentPage == 2) paymentMethodsRecyclerView.visibility = View.VISIBLE + } + + private fun animateHideWarning(textView: TextView) { + val animation = AnimationUtils.loadAnimation(view.context, R.anim.fast_fade_out_animation) + textView.animation = animation + } + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + lottieViewPortrait?.progress = + position * (1f / ANIMATION_TRANSITIONS) + positionOffset * (1f / ANIMATION_TRANSITIONS) + lottieViewLandscape?.progress = + position * (1f / ANIMATION_TRANSITIONS) + positionOffset * (1f / ANIMATION_TRANSITIONS) + checkBox.setOnClickListener { handleUI(position) } + updatePageIndicator(position) + currentPage = position + handleUI(position) + } + + private fun handleUI(position: Int) { + if (position < 3) { + showFirstPageLayout() + } else if (position == 3) { + showLastPageLayout() + } + + if (position == 2 && paymentMethodsIcons.isNotEmpty()) { + paymentMethodsRecyclerView.visibility = View.VISIBLE + } else { + paymentMethodsRecyclerView.visibility = View.GONE + } + } + + private fun showLastPageLayout() { + skipButton.visibility = View.GONE + nextButton.visibility = View.VISIBLE + if (isActive) beenInvitedButton.visibility = View.VISIBLE + termsConditionsLayout.visibility = View.VISIBLE + nextButton.isEnabled = checkBox.isChecked + beenInvitedButton.isEnabled = checkBox.isChecked + + if (checkBox.isChecked) { + if (warningText.visibility == View.VISIBLE) { + animateHideWarning(warningText) + warningText.visibility = View.INVISIBLE + } + } + } + + private fun showFirstPageLayout() { + skipButton.visibility = View.VISIBLE + nextButton.visibility = View.GONE + beenInvitedButton.visibility = View.GONE + termsConditionsLayout.visibility = View.GONE + + if (warningText.visibility == View.VISIBLE) { + animateHideWarning(warningText) + warningText.visibility = View.INVISIBLE + } + } + + private fun updatePageIndicator(position: Int) { + val pos: Int + val config = view.resources.configuration + pos = if (config.layoutDirection == View.LAYOUT_DIRECTION_LTR) { + position + } else { + PAGE_COUNT - position - 1 + } + pageIndicatorView.setSelected(pos) + } + + override fun onPageSelected(position: Int) = Unit + + override fun onPageScrollStateChanged(state: Int) = Unit +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPageTransformer.kt b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPageTransformer.kt new file mode 100644 index 00000000000..b888c0b98e3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPageTransformer.kt @@ -0,0 +1,44 @@ +package com.asfoundation.wallet.ui.onboarding + +import android.view.View +import androidx.viewpager2.widget.ViewPager2 +import kotlin.math.abs + +class OnboardingPageTransformer : ViewPager2.PageTransformer { + + override fun transformPage(view: View, position: Float) { + val pageWidth = view.width + + when { + position < -1 -> // [-Infinity,-1) + // This page is way off-screen to the left. + view.alpha = 0f + position <= 0 -> { // [-1,0] + // Use the default slide transition when moving to the left page + view.alpha = 1f + view.translationX = 0f + view.scaleX = 1f + view.scaleY = 1f + } + position <= 1 -> { // (0,1] + // Fade the page out. + view.alpha = 1 - position + + // Counteract the default slide transition + view.translationX = pageWidth * -position + + // Scale the page down (between MIN_SCALE and 1) + val scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - abs(position)) + view.scaleX = scaleFactor + view.scaleY = scaleFactor + } + else -> // (1,+Infinity] + // This page is way off-screen to the right. + view.alpha = 0f + } + } + + companion object { + private const val MIN_SCALE = 0.75f + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPaymentMethodAdapter.kt b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPaymentMethodAdapter.kt new file mode 100644 index 00000000000..ddade18ebcc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPaymentMethodAdapter.kt @@ -0,0 +1,24 @@ +package com.asfoundation.wallet.ui.onboarding + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R + +class OnboardingPaymentMethodAdapter(private var items: List) : + RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, + viewType: Int): OnboardingPaymentMethodViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.layout_page_intro_payment_icon, parent, false) + return OnboardingPaymentMethodViewHolder(view) + } + + override fun onBindViewHolder(holder: OnboardingPaymentMethodViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.size + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPaymentMethodViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPaymentMethodViewHolder.kt new file mode 100644 index 00000000000..de5a0a44011 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPaymentMethodViewHolder.kt @@ -0,0 +1,20 @@ +package com.asfoundation.wallet.ui.onboarding + +import android.view.View +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import com.asfoundation.wallet.GlideApp +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import kotlinx.android.synthetic.main.layout_page_intro_payment_icon.view.* + +class OnboardingPaymentMethodViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + fun bind(imageSrc: String) { + val imageView = itemView.payment_icon as ImageView + + GlideApp.with(itemView) + .load(imageSrc) + .transition(DrawableTransitionOptions.withCrossFade()) + .into(imageView) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPresenter.kt new file mode 100644 index 00000000000..53f0b1c819c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingPresenter.kt @@ -0,0 +1,184 @@ +package com.asfoundation.wallet.ui.onboarding + +import android.net.Uri +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.util.scaleToString +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.Function3 +import io.reactivex.subjects.ReplaySubject +import java.util.concurrent.TimeUnit + +class OnboardingPresenter(private val disposables: CompositeDisposable, + private val view: OnboardingView, + private val onboardingInteract: OnboardingInteract, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val walletCreated: ReplaySubject, + private val logger: Logger) { + + private var hasShowedWarning = false + + fun present() { + handleAvailablePaymentMethods() + handleSetupUI() + handleSkipClicks() + handleSkippedOnboarding() + handleLinkClick() + handleCreateWallet() + handleRedeemButtonClicks() + handleNextButtonClicks() + handleLaterClicks() + handleRetryClicks() + handleWarningText() + } + + private fun handleAvailablePaymentMethods() { + disposables.add(onboardingInteract.getPaymentMethodsIcons() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { view.setPaymentMethodsIcons(it) } + .subscribe({}, { logger.log(TAG, it) })) + } + + private fun handleSetupUI() { + disposables.add(onboardingInteract.getReferralInfo() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { view.updateUI(it.symbol + it.maxAmount.scaleToString(2), it.isActive) } + .subscribe({}, { logger.log(TAG, it) }) + ) + } + + fun markedWarningTextAsShowed() { + hasShowedWarning = true + } + + private fun handleWarningText() { + disposables.add(Observable.timer(5, TimeUnit.SECONDS) + .observeOn(viewScheduler) + .doOnNext { view.showWarningText() } + .repeatUntil { hasShowedWarning } + .subscribe({}, { it.printStackTrace() }) + ) + } + + fun stop() = disposables.clear() + + private fun handleSkipClicks() { + disposables.add(view.getSkipClicks() + .doOnNext { view.showViewPagerLastPage() } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun isWalletCreated() = walletCreated.filter { it } + + private fun handleRetryClicks() { + disposables.add(view.getRetryButtonClicks() + .doOnNext { handleWalletCreation(skipValidation = false, showAnimation = false) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleLaterClicks() { + disposables.add(view.getLaterButtonClicks() + .doOnNext { handleValidationStatus(WalletValidationStatus.SUCCESS, false) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleRedeemButtonClicks() { + disposables.add(view.getRedeemButtonClick() + .observeOn(viewScheduler) + .doOnNext { handleWalletCreation(skipValidation = false, showAnimation = true) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleWalletCreation(skipValidation: Boolean, showAnimation: Boolean) { + if (walletCreated.hasValue() || !showAnimation) { + handleFinishNavigation(skipValidation, false, 0) + } else { + view.showLoading() + handleFinishNavigation(skipValidation, showAnimation, 1) + } + } + + private fun handleFinishNavigation(skipValidation: Boolean, showAnimation: Boolean, delay: Long) { + disposables.add(isWalletCreated() + .flatMapSingle { onboardingInteract.getWalletAddress() } + .flatMapSingle { + if (skipValidation) { + Single.just(WalletValidationStatus.SUCCESS) + } else { + onboardingInteract.isAddressValid(it) + .subscribeOn(networkScheduler) + } + } + .delay(delay, TimeUnit.SECONDS) + .observeOn(viewScheduler) + .doOnNext { handleValidationStatus(it, showAnimation) } + .subscribe({}, { logger.log(TAG, it) })) + } + + private fun handleValidationStatus(walletValidationStatus: WalletValidationStatus, + showAnimation: Boolean) { + if (walletValidationStatus == WalletValidationStatus.NO_NETWORK) { + view.showNoInternetView() + } else { + finishOnBoarding(walletValidationStatus, showAnimation) + } + } + + private fun handleNextButtonClicks() { + disposables.add(view.getNextButtonClick() + .doOnNext { handleWalletCreation(skipValidation = true, showAnimation = true) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleCreateWallet() { + disposables.add(onboardingInteract.getWalletAddress() + .observeOn(viewScheduler) + .flatMapCompletable { Completable.fromAction { walletCreated.onNext(true) } } + .subscribe({}, { logger.log(TAG, it) })) + } + + private fun handleSkippedOnboarding() { + disposables.add(Observable.zip(isWalletCreated(), + Observable.fromCallable { onboardingInteract.hasClickedSkipOnboarding() } + .filter { clicked -> clicked }, + Observable.fromCallable { onboardingInteract.hasOnboardingCompleted() } + .filter { clicked -> clicked }, + Function3 { _: Any, _: Any, _: Any -> } + ) + .delay(1, TimeUnit.SECONDS) + .observeOn(viewScheduler) + .doOnNext { handleValidationStatus(WalletValidationStatus.SUCCESS, true) } + .subscribe({}, { logger.log(TAG, it) }) + ) + } + + private fun handleLinkClick() { + disposables.add(view.getLinkClick() + .doOnNext { uri -> view.navigateToBrowser(Uri.parse(uri)) } + .subscribe({}, { it.printStackTrace() }) + ) + } + + fun markOnboardingCompleted() = onboardingInteract.finishOnboarding() + + private fun finishOnBoarding(walletValidationStatus: WalletValidationStatus, + showAnimation: Boolean) { + onboardingInteract.clickSkipOnboarding() + view.finishOnboarding(walletValidationStatus, showAnimation) + } + + companion object { + private val TAG = OnboardingPresenter::class.java.name + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingView.kt b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingView.kt new file mode 100644 index 00000000000..96b40f02246 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingView.kt @@ -0,0 +1,37 @@ +package com.asfoundation.wallet.ui.onboarding + +import android.net.Uri +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import io.reactivex.Observable + +interface OnboardingView { + + fun updateUI(maxAmount: String, isActive: Boolean) + + fun showLoading() + + fun finishOnboarding(walletValidationStatus: WalletValidationStatus, showAnimation: Boolean) + + fun getNextButtonClick(): Observable + + fun getRedeemButtonClick(): Observable + + fun getLinkClick(): Observable + + fun getSkipClicks(): Observable + + fun navigateToBrowser(uri: Uri) + + fun showNoInternetView() + + fun showViewPagerLastPage() + + fun setPaymentMethodsIcons(paymentMethodsIcons: List) + + fun getRetryButtonClicks(): Observable + + fun getLaterButtonClicks(): Observable + + fun showWarningText() + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingViewHolder.kt new file mode 100644 index 00000000000..7b1c7beeb2d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/onboarding/OnboardingViewHolder.kt @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.ui.onboarding + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.layout_page_intro.view.* + +class OnboardingViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + fun bind(item: OnboardingItem) { + itemView.title.setText(item.title) + itemView.message.text = item.message + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/toolbar/ToolbarArcBackground.java b/app/src/main/java/com/asfoundation/wallet/ui/toolbar/ToolbarArcBackground.java index cc636cd79e2..3d94ac111b9 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/toolbar/ToolbarArcBackground.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/toolbar/ToolbarArcBackground.java @@ -4,7 +4,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.View; import com.asf.wallet.R; diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/AppcoinsCreditsTransactSuccessPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/AppcoinsCreditsTransactSuccessPresenter.kt new file mode 100644 index 00000000000..65f8fa982d2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/AppcoinsCreditsTransactSuccessPresenter.kt @@ -0,0 +1,38 @@ +package com.asfoundation.wallet.ui.transact + +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import io.reactivex.disposables.CompositeDisposable +import java.math.BigDecimal + +class AppcoinsCreditsTransactSuccessPresenter(private val view: AppcoinsCreditsTransactSuccessView, + private val amount: BigDecimal, + private val currency: String, + private val toAddress: String, + private val disposables: CompositeDisposable, + private val formatter: CurrencyFormatUtils) { + fun present() { + val walletCurrency = mapToWalletCurrency(currency) + val formattedAmount = formatter.formatTransferCurrency(amount, walletCurrency) + view.setup(formattedAmount, walletCurrency.symbol, toAddress) + handleOkButtonClick() + } + + private fun handleOkButtonClick() { + disposables.add(view.getOkClick() + .doOnNext { view.close() } + .subscribe()) + } + + fun stop() { + disposables.clear() + } + + private fun mapToWalletCurrency(currency: String): WalletCurrency { + return when (currency) { + "APPC" -> WalletCurrency.APPCOINS + "APPC-C" -> WalletCurrency.CREDITS + else -> WalletCurrency.ETHEREUM + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/AppcoinsCreditsTransactSuccessView.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/AppcoinsCreditsTransactSuccessView.kt new file mode 100644 index 00000000000..da983ed634c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/AppcoinsCreditsTransactSuccessView.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.ui.transact + +import io.reactivex.Observable + +interface AppcoinsCreditsTransactSuccessView { + + fun setup(amount: String, currency: String, toAddress: String) + + fun getOkClick(): Observable + + fun close() + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/AppcoinsCreditsTransferSuccessFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/AppcoinsCreditsTransferSuccessFragment.kt new file mode 100644 index 00000000000..f14313c3d53 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/AppcoinsCreditsTransferSuccessFragment.kt @@ -0,0 +1,88 @@ +package com.asfoundation.wallet.ui.transact + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.ui.ActivityResultSharer +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.transact_success_fragment_layout.* +import java.math.BigDecimal +import javax.inject.Inject + +class AppcoinsCreditsTransferSuccessFragment : BasePageViewFragment(), + AppcoinsCreditsTransactSuccessView { + companion object { + private const val AMOUNT_SENT_KEY = "AMOUNT_SENT" + private const val CURRENCY_KEY = "CURRENCY" + private const val TO_ADDRESS_KEY = "TO_ADDRESS" + + fun newInstance(amount: BigDecimal, currency: String, + toAddress: String): AppcoinsCreditsTransferSuccessFragment = + AppcoinsCreditsTransferSuccessFragment().apply { + arguments = Bundle(3).apply { + putSerializable(AMOUNT_SENT_KEY, amount) + putString(CURRENCY_KEY, currency) + putString(TO_ADDRESS_KEY, toAddress) + } + } + } + + @Inject + lateinit var formatter: CurrencyFormatUtils + private lateinit var presenter: AppcoinsCreditsTransactSuccessPresenter + private lateinit var navigator: TransactNavigator + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val amount = arguments!!.getSerializable(AMOUNT_SENT_KEY) as BigDecimal + val currency = arguments!!.getString(CURRENCY_KEY)!! + val toAddress = arguments!!.getString(TO_ADDRESS_KEY)!! + presenter = AppcoinsCreditsTransactSuccessPresenter(this, amount, currency, toAddress, + CompositeDisposable(), formatter) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.transact_success_fragment_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present() + } + + override fun getOkClick(): Observable { + return RxView.clicks(transfer_success_ok_button) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + when (context) { + is TransactNavigator -> navigator = context + else -> throw IllegalArgumentException( + "${this.javaClass.simpleName} has to be attached to an activity that implements ${ActivityResultSharer::class}") + } + } + + override fun close() { + navigator.closeScreen() + } + + override fun setup(amount: String, currency: String, toAddress: String) { + transfer_success_wallet.text = toAddress + transfer_success_message.text = + getString(R.string.p2p_send_confirmation_message, amount, currency) + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/LoadingFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/LoadingFragment.kt new file mode 100644 index 00000000000..3f705dab401 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/LoadingFragment.kt @@ -0,0 +1,22 @@ +package com.asfoundation.wallet.ui.transact + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.asf.wallet.R + +class LoadingFragment : Fragment() { + companion object { + fun newInstance(): Fragment { + return LoadingFragment() + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.transact_loading_view, container, false) + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/TransactNavigator.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransactNavigator.kt new file mode 100644 index 00000000000..e774f737991 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransactNavigator.kt @@ -0,0 +1,15 @@ +package com.asfoundation.wallet.ui.transact + +import java.math.BigDecimal + +interface TransactNavigator { + fun openAppcoinsCreditsSuccess(walletAddress: String, amount: BigDecimal, + currency: String) + + fun showLoading() + fun hideLoading() + fun closeScreen() + fun hideKeyboard() + fun openQrCodeScreen() + fun showWalletBlocked() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/TransactionDataValidator.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransactionDataValidator.kt new file mode 100644 index 00000000000..8e864c3ebba --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransactionDataValidator.kt @@ -0,0 +1,23 @@ +package com.asfoundation.wallet.ui.transact + +import com.asfoundation.wallet.entity.Address +import java.math.BigDecimal + +class TransactionDataValidator { + fun validateData(toWallet: String, amount: BigDecimal, balance: BigDecimal): DataStatus { + if (!Address.isAddress(toWallet)) { + return DataStatus.INVALID_WALLET_ADDRESS + } + if (amount.compareTo(BigDecimal.ZERO) < 1) { + return DataStatus.INVALID_AMOUNT + } + if (amount.compareTo(balance) == 1) { + return DataStatus.NOT_ENOUGH_FUNDS + } + return DataStatus.OK + } + + enum class DataStatus { + OK, INVALID_AMOUNT, INVALID_WALLET_ADDRESS, NOT_ENOUGH_FUNDS + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferActivity.kt new file mode 100644 index 00000000000..b8da3e63bd2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferActivity.kt @@ -0,0 +1,102 @@ +package com.asfoundation.wallet.ui.transact + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.view.View +import android.view.inputmethod.InputMethodManager +import com.asf.wallet.R +import com.asfoundation.wallet.ui.BaseActivity +import com.asfoundation.wallet.ui.barcode.BarcodeCaptureActivity +import com.asfoundation.wallet.ui.iab.IabActivity +import com.asfoundation.wallet.wallet_blocked.WalletBlockedActivity +import java.math.BigDecimal + +class TransferActivity : BaseActivity(), TransferActivityView, TransactNavigator { + + private lateinit var presenter: TransferActivityPresenter + + companion object { + const val BARCODE_READER_REQUEST_CODE = 1 + + @JvmStatic + fun newIntent(context: Context): Intent { + return Intent(context, TransferActivity::class.java) + } + } + + override fun closeScreen() { + finish() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.transaction_activity_layout) + presenter = TransferActivityPresenter(this) + presenter.present(savedInstanceState == null) + toolbar() + } + + override fun showTransactFragment() { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, TransferFragment.newInstance()) + .commit() + } + + override fun showLoading() { + lockOrientation() + supportFragmentManager.beginTransaction() + .add(android.R.id.content, LoadingFragment.newInstance(), + LoadingFragment::class.java.name) + .commit() + } + + override fun hideLoading() { + val fragment = + supportFragmentManager.findFragmentByTag(LoadingFragment::class.java.name) + if (fragment != null) { + supportFragmentManager.beginTransaction() + .remove(fragment) + .commit() + } + unlockOrientation() + } + + override fun showWalletBlocked() { + startActivityForResult(WalletBlockedActivity.newIntent(this), + IabActivity.BLOCKED_WARNING_REQUEST_CODE) + } + + private fun lockOrientation() { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED + } + + private fun unlockOrientation() { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } + + override fun openAppcoinsCreditsSuccess(walletAddress: String, amount: BigDecimal, + currency: String) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + AppcoinsCreditsTransferSuccessFragment.newInstance(amount, currency, walletAddress)) + .commit() + } + + override fun openQrCodeScreen() { + val intent = Intent(this, BarcodeCaptureActivity::class.java) + startActivityForResult(intent, BARCODE_READER_REQUEST_CODE) + } + + override fun hideKeyboard() { + val inputMethodManager = + this.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + var view = this.currentFocus + if (view == null) { + view = View(this) + } + inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferActivityPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferActivityPresenter.kt new file mode 100644 index 00000000000..ca2955a10d0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferActivityPresenter.kt @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.ui.transact + +class TransferActivityPresenter(private val view: TransferActivityView) { + fun present(isCreating: Boolean) { + if (isCreating) { + view.showTransactFragment() + } + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferActivityView.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferActivityView.kt new file mode 100644 index 00000000000..8fec6dbbf60 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferActivityView.kt @@ -0,0 +1,5 @@ +package com.asfoundation.wallet.ui.transact + +interface TransferActivityView { + fun showTransactFragment() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferFragment.kt new file mode 100644 index 00000000000..754a03d10ea --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferFragment.kt @@ -0,0 +1,295 @@ +package com.asfoundation.wallet.ui.transact + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.TextView +import android.widget.Toast +import android.widget.Toast.LENGTH_SHORT +import com.asf.wallet.R +import com.asfoundation.wallet.entity.TokenInfo +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.interact.DefaultTokenProvider +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.router.ConfirmationRouter +import com.asfoundation.wallet.ui.ActivityResultSharer +import com.asfoundation.wallet.ui.barcode.BarcodeCaptureActivity +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.vision.barcode.Barcode +import com.google.android.material.snackbar.Snackbar +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxbinding2.widget.RxRadioGroup +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.currency_choose_layout.* +import kotlinx.android.synthetic.main.transact_fragment_layout.* +import java.math.BigDecimal +import java.util.* +import javax.inject.Inject + +class TransferFragment : BasePageViewFragment(), TransferFragmentView { + + companion object { + fun newInstance() = TransferFragment() + } + + private lateinit var presenter: TransferPresenter + + @Inject + lateinit var interactor: TransferInteractor + + @Inject + lateinit var confirmationRouter: ConfirmationRouter + + @Inject + lateinit var findDefaultWalletInteract: FindDefaultWalletInteract + + @Inject + lateinit var defaultTokenInfoProvider: DefaultTokenProvider + + @Inject + lateinit var walletBlockedInteract: WalletBlockedInteract + + @Inject + lateinit var formatter: CurrencyFormatUtils + + lateinit var navigator: TransactNavigator + private lateinit var activityResultSharer: ActivityResultSharer + private lateinit var doneClick: PublishSubject + private lateinit var qrCodeResult: BehaviorSubject + private var disposable: Disposable? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + doneClick = PublishSubject.create() + qrCodeResult = BehaviorSubject.create() + disposable = + confirmationRouter.transactionResult + .doOnNext { activity?.onBackPressed() } + .subscribe() + presenter = TransferPresenter(this, CompositeDisposable(), CompositeDisposable(), interactor, + Schedulers.io(), AndroidSchedulers.mainThread(), findDefaultWalletInteract, + walletBlockedInteract, context!!.packageName, formatter) + } + + override fun openEthConfirmationView(walletAddress: String, toWalletAddress: String, + amount: BigDecimal): Completable { + return Completable.fromAction { + val transaction = TransactionBuilder(TokenInfo(null, "Ethereum", "ETH", 18)) + transaction.amount(amount) + transaction.toAddress(toWalletAddress) + transaction.fromAddress(walletAddress) + confirmationRouter.open(activity, transaction) + } + } + + override fun openAppcConfirmationView(walletAddress: String, toWalletAddress: String, + amount: BigDecimal): Completable { + + return defaultTokenInfoProvider.defaultToken.doOnSuccess { + with(TransactionBuilder(it)) { + amount(amount) + toAddress(toWalletAddress) + fromAddress(walletAddress) + confirmationRouter.open(activity, this) + } + } + .ignoreElement() + } + + override fun openAppcCreditsConfirmationView(walletAddress: String, + amount: BigDecimal, + currency: TransferFragmentView.Currency): Completable { + return Completable.fromAction { + val currencyName = when (currency) { + TransferFragmentView.Currency.APPC_C -> getString(R.string.p2p_send_currency_appc_c) + TransferFragmentView.Currency.APPC -> getString(R.string.p2p_send_currency_appc) + TransferFragmentView.Currency.ETH -> getString(R.string.p2p_send_currency_eth) + } + navigator.openAppcoinsCreditsSuccess(walletAddress, amount, currencyName) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.transact_fragment_layout, container, false) + } + + override fun getCurrencyChange(): Observable { + return RxRadioGroup.checkedChanges(currency_selector) + .map { map(currency_selector.checkedRadioButtonId) } + } + + override fun showBalance(balance: String, + currency: WalletCurrency) { + transact_fragment_balance.text = + getString(R.string.p2p_send_current_balance_message, balance, currency.symbol) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present() + transact_fragment_amount.setOnEditorActionListener( + TextView.OnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + navigator.hideKeyboard() + doneClick.onNext(Any()) + return@OnEditorActionListener true + } + return@OnEditorActionListener false + }) + } + + override fun showWalletBlocked() = navigator.showWalletBlocked() + + override fun onResume() { + super.onResume() + presenter.onResume() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + when (context) { + is ActivityResultSharer -> activityResultSharer = context + else -> throw IllegalArgumentException( + "${this.javaClass.simpleName} has to be attached to an activity that implements ${ActivityResultSharer::class}") + } + when (context) { + is TransactNavigator -> navigator = context + else -> throw IllegalArgumentException( + "${this.javaClass.simpleName} has to be attached to an activity that implements ${ActivityResultSharer::class}") + } + activityResultSharer.addOnActivityListener(confirmationRouter) + activityResultSharer.addOnActivityListener(object : + ActivityResultSharer.ActivityResultListener { + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + if (resultCode == CommonStatusCodes.SUCCESS && requestCode == TransferActivity.BARCODE_READER_REQUEST_CODE) { + data?.let { + val barcode = it.getParcelableExtra(BarcodeCaptureActivity.BarcodeObject) + println(barcode) + barcode?.let { mBarcode -> qrCodeResult.onNext(mBarcode) } + return true + } + } + return false + } + }) + } + + override fun showCameraErrorToast() { + Toast.makeText(context, R.string.toast_qr_code_no_address, LENGTH_SHORT) + .show() + } + + override fun showAddress(address: String) { + transact_fragment_recipient_address.setText(address) + } + + override fun getQrCodeResult(): Observable { + return qrCodeResult + } + + override fun getSendClick(): Observable { + return Observable.merge(doneClick, RxView.clicks(send_button)) + .map { + var amount = BigDecimal.ZERO + if (transact_fragment_amount.text.toString() + .isNotEmpty()) { + amount = transact_fragment_amount.text.toString() + .toBigDecimal() + } + TransferFragmentView.TransferData( + transact_fragment_recipient_address.text.toString() + .toLowerCase(Locale.ROOT), + map(currency_selector.checkedRadioButtonId), amount) + } + } + + override fun showInvalidWalletAddress() { + transact_fragment_amount_layout.error = null + transact_fragment_recipient_address_layout.error = getString(R.string.p2p_send_error_address) + } + + override fun getQrCodeButtonClick(): Observable { + return RxView.clicks(scan_barcode_button) + } + + override fun showQrCodeScreen() { + navigator.openQrCodeScreen() + } + + override fun showUnknownError() { + Snackbar.make(title, R.string.error_general, Snackbar.LENGTH_LONG) + .show() + } + + override fun showNoNetworkError() { + Snackbar.make(title, R.string.connectoin_error_body, Snackbar.LENGTH_LONG) + .show() + } + + override fun showInvalidAmountError() { + transact_fragment_recipient_address_layout.error = null + transact_fragment_amount_layout.error = getString(R.string.p2p_send_error_amount_zero) + } + + override fun showNotEnoughFunds() { + transact_fragment_recipient_address_layout.error = null + transact_fragment_amount_layout.error = getString(R.string.p2p_send_error_not_enough_funds) + } + + override fun showLoading() { + navigator.showLoading() + } + + override fun hideLoading() { + navigator.hideLoading() + } + + override fun onDetach() { + activityResultSharer.remove(confirmationRouter) + super.onDetach() + } + + override fun onPause() { + presenter.clearOnPause() + super.onPause() + } + + private fun map(checkedRadioButtonId: Int): TransferFragmentView.Currency { + return when (checkedRadioButtonId) { + R.id.appcoins_credits_radio_button -> TransferFragmentView.Currency.APPC_C + R.id.appcoins_radio_button -> TransferFragmentView.Currency.APPC + R.id.ethereum_credits_radio_button -> TransferFragmentView.Currency.ETH + else -> throw UnsupportedOperationException("Unknown selected currency") + } + } + + override fun onDestroy() { + disposable?.takeIf { + !it.isDisposed + } + .let { it?.dispose() } + super.onDestroy() + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferFragmentView.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferFragmentView.kt new file mode 100644 index 00000000000..e82e4b8c1b2 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferFragmentView.kt @@ -0,0 +1,59 @@ +package com.asfoundation.wallet.ui.transact + +import com.asfoundation.wallet.util.WalletCurrency +import com.google.android.gms.vision.barcode.Barcode +import io.reactivex.Completable +import io.reactivex.Observable +import java.io.Serializable +import java.math.BigDecimal + +interface TransferFragmentView { + + fun getSendClick(): Observable + + fun openEthConfirmationView(walletAddress: String, toWalletAddress: String, + amount: BigDecimal): Completable + + fun openAppcConfirmationView(walletAddress: String, toWalletAddress: String, + amount: BigDecimal): Completable + + fun openAppcCreditsConfirmationView(walletAddress: String, amount: BigDecimal, + currency: Currency): Completable + + fun showLoading() + + fun hideLoading() + + fun showInvalidAmountError() + + fun showInvalidWalletAddress() + + fun showNotEnoughFunds() + + fun showUnknownError() + + fun getQrCodeButtonClick(): Observable + + fun showQrCodeScreen() + + fun getQrCodeResult(): Observable + + fun showAddress(address: String) + + fun getCurrencyChange(): Observable + + fun showBalance(balance: String, currency: WalletCurrency) + + fun showWalletBlocked() + + fun showNoNetworkError() + + data class TransferData(val walletAddress: String, val currency: Currency, + val amount: BigDecimal) : Serializable + + enum class Currency { + APPC_C, APPC, ETH + } + + fun showCameraErrorToast() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferInteractor.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferInteractor.kt new file mode 100644 index 00000000000..bc192d05e16 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferInteractor.kt @@ -0,0 +1,82 @@ +package com.asfoundation.wallet.ui.transact + +import com.appcoins.wallet.appcoins.rewards.AppcoinsRewardsRepository +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.interact.GetDefaultWalletBalanceInteract +import com.asfoundation.wallet.ui.iab.RewardsManager +import com.asfoundation.wallet.util.BalanceUtils +import io.reactivex.Single +import java.math.BigDecimal +import java.math.RoundingMode +import java.net.UnknownHostException + +class TransferInteractor(private val rewardsManager: RewardsManager, + private val transactionDataValidator: TransactionDataValidator, + private val balanceInteractor: GetDefaultWalletBalanceInteract, + private val findDefaultWalletInteract: FindDefaultWalletInteract) { + + fun transferCredits(toWallet: String, amount: BigDecimal, + packageName: String): Single { + return rewardsManager.balance.map { + transactionDataValidator.validateData(toWallet, amount.multiply(BigDecimal.TEN.pow(18)), it) + } + .flatMap { + val validateStatus = validateData(it) + if (validateStatus == AppcoinsRewardsRepository.Status.SUCCESS) { + return@flatMap rewardsManager.sendCredits(toWallet, amount, packageName) + } + return@flatMap Single.just(validateStatus) + } + .onErrorReturn { map(it) } + } + + private fun validateData( + data: TransactionDataValidator.DataStatus): AppcoinsRewardsRepository.Status { + return when (data) { + TransactionDataValidator.DataStatus.OK -> AppcoinsRewardsRepository.Status.SUCCESS + TransactionDataValidator.DataStatus.INVALID_AMOUNT -> AppcoinsRewardsRepository.Status.INVALID_AMOUNT + TransactionDataValidator.DataStatus.INVALID_WALLET_ADDRESS -> AppcoinsRewardsRepository.Status.INVALID_WALLET_ADDRESS + TransactionDataValidator.DataStatus.NOT_ENOUGH_FUNDS -> AppcoinsRewardsRepository.Status.NOT_ENOUGH_FUNDS + } + } + + private fun map(throwable: Throwable): AppcoinsRewardsRepository.Status { + return when (throwable) { + is UnknownHostException -> return AppcoinsRewardsRepository.Status.NO_INTERNET + else -> AppcoinsRewardsRepository.Status.UNKNOWN_ERROR + } + } + + fun getCreditsBalance(): Single { + return rewardsManager.balance.map { + BalanceUtils.weiToEth(it) + .setScale(4, RoundingMode.FLOOR) + } + } + + fun getAppcoinsBalance(): Single { + return findDefaultWalletInteract.find() + .flatMap { balanceInteractor.getAppcBalance(it.address) } + .map { it.value } + } + + fun getEthBalance(): Single { + return findDefaultWalletInteract.find() + .flatMap { balanceInteractor.getEthereumBalance(it.address) } + .map { it.value } + } + + fun validateEthTransferData(walletAddress: String, + amount: BigDecimal): Single { + return getEthBalance().map { + validateData(transactionDataValidator.validateData(walletAddress, amount, it)) + } + } + + fun validateAppcTransferData(walletAddress: String, + amount: BigDecimal): Single { + return getAppcoinsBalance().map { + validateData(transactionDataValidator.validateData(walletAddress, amount, it)) + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferPresenter.kt new file mode 100644 index 00000000000..53943454d97 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/transact/TransferPresenter.kt @@ -0,0 +1,179 @@ +package com.asfoundation.wallet.ui.transact + +import com.appcoins.wallet.appcoins.rewards.AppcoinsRewardsRepository.Status +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.ui.barcode.BarcodeCaptureActivity +import com.asfoundation.wallet.ui.transact.TransferFragmentView.Currency +import com.asfoundation.wallet.ui.transact.TransferFragmentView.TransferData +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.QRUri +import com.asfoundation.wallet.util.WalletCurrency +import com.asfoundation.wallet.util.isNoNetworkException +import com.asfoundation.wallet.wallet_blocked.WalletBlockedInteract +import io.reactivex.Completable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.BiFunction +import java.math.BigDecimal +import java.util.concurrent.TimeUnit + +class TransferPresenter(private val view: TransferFragmentView, + private val disposables: CompositeDisposable, + private val onResumeDisposables: CompositeDisposable, + private val interactor: TransferInteractor, + private val ioScheduler: Scheduler, + private val viewScheduler: Scheduler, + private val walletInteract: FindDefaultWalletInteract, + private val walletBlockedInteract: WalletBlockedInteract, + private val packageName: String, + private val formatter: CurrencyFormatUtils) { + + fun onResume() { + handleQrCodeResult() + handleCurrencyChange() + } + + fun present() { + handleButtonClick() + handleQrCodeButtonClick() + } + + private fun handleCurrencyChange() { + onResumeDisposables.add(view.getCurrencyChange() + .subscribeOn(viewScheduler) + .observeOn(ioScheduler) + .switchMapSingle { currency -> + getBalance(currency) + .observeOn(viewScheduler) + .doOnSuccess { + val walletCurrency = WalletCurrency.mapToWalletCurrency(currency) + view.showBalance(formatter.formatCurrency(it, walletCurrency), walletCurrency) + } + } + .doOnError { it.printStackTrace() } + .retry() + .subscribe({}, { it.printStackTrace() })) + } + + private fun getBalance(currency: Currency): Single { + return when (currency) { + Currency.APPC_C -> interactor.getCreditsBalance() + Currency.APPC -> interactor.getAppcoinsBalance() + Currency.ETH -> interactor.getEthBalance() + } + } + + private fun handleQrCodeResult() { + onResumeDisposables.add(view.getQrCodeResult() + .observeOn(ioScheduler) + .map { QRUri.parse(it.displayValue) } + .observeOn(viewScheduler) + .doOnNext { handleQRUri(it) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleQRUri(qrUri: QRUri) { + if (qrUri.address != BarcodeCaptureActivity.ERROR_CODE) { + view.showAddress(qrUri.address) + } else { + view.showCameraErrorToast() + } + } + + private fun handleQrCodeButtonClick() { + disposables.add(view.getQrCodeButtonClick() + .doOnNext { view.showQrCodeScreen() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun shouldBlockTransfer(currency: Currency): Single { + return if (currency == Currency.APPC_C) { + walletBlockedInteract.isWalletBlocked() + } else { + Single.just(false) + } + } + + private fun handleButtonClick() { + disposables.add(view.getSendClick() + .doOnNext { view.showLoading() } + .subscribeOn(viewScheduler) + .observeOn(ioScheduler) + .flatMapCompletable { data -> + shouldBlockTransfer(data.currency) + .flatMapCompletable { + if (it) { + Completable.fromAction { view.showWalletBlocked() } + .subscribeOn(viewScheduler) + } else { + makeTransaction(data) + .observeOn(viewScheduler) + .flatMapCompletable { status -> + handleTransferResult(data.currency, status, data.walletAddress, + data.amount) + } + .andThen { view.hideLoading() } + } + } + } + .doOnError { handleError(it) } + .retry() + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleError(throwable: Throwable) { + view.hideLoading() + if (throwable.isNoNetworkException()) { + view.showNoNetworkError() + } else { + throwable.printStackTrace() + view.showUnknownError() + } + } + + private fun makeTransaction(data: TransferData): Single { + return when (data.currency) { + Currency.APPC_C -> handleCreditsTransfer(data.walletAddress, data.amount) + Currency.ETH -> interactor.validateEthTransferData(data.walletAddress, data.amount) + Currency.APPC -> interactor.validateAppcTransferData(data.walletAddress, data.amount) + } + } + + private fun handleTransferResult(currency: Currency, status: Status, + walletAddress: String, amount: BigDecimal): Completable { + return Single.just(status) + .subscribeOn(viewScheduler) + .flatMapCompletable { + when (status) { + Status.API_ERROR, Status.UNKNOWN_ERROR, Status.NO_INTERNET -> Completable.fromCallable { view.showUnknownError() } + Status.SUCCESS -> handleSuccess(currency, walletAddress, amount) + Status.INVALID_AMOUNT -> Completable.fromCallable { view.showInvalidAmountError() } + Status.INVALID_WALLET_ADDRESS -> Completable.fromCallable { view.showInvalidWalletAddress() } + Status.NOT_ENOUGH_FUNDS -> Completable.fromCallable { view.showNotEnoughFunds() } + } + } + } + + private fun handleSuccess(currency: Currency, walletAddress: String, + amount: BigDecimal): Completable { + return when (currency) { + Currency.APPC_C -> view.openAppcCreditsConfirmationView(walletAddress, amount, currency) + Currency.APPC -> walletInteract.find() + .flatMapCompletable { view.openAppcConfirmationView(it.address, walletAddress, amount) } + Currency.ETH -> walletInteract.find() + .flatMapCompletable { view.openEthConfirmationView(it.address, walletAddress, amount) } + } + } + + private fun handleCreditsTransfer(walletAddress: String, + amount: BigDecimal): Single { + return Single.zip(Single.timer(1, TimeUnit.SECONDS), + interactor.transferCredits(walletAddress, amount, packageName), + BiFunction { _: Long, status: Status -> status }) + } + + fun clearOnPause() = onResumeDisposables.clear() + + fun stop() = disposables.clear() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletActivity.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletActivity.kt new file mode 100644 index 00000000000..969ad736d06 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletActivity.kt @@ -0,0 +1,158 @@ +package com.asfoundation.wallet.ui.wallets + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import com.asf.wallet.R +import com.asfoundation.wallet.ui.AuthenticationPromptActivity +import com.asfoundation.wallet.ui.BaseActivity +import com.asfoundation.wallet.ui.backup.WalletBackupActivity.Companion.newIntent +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.remove_wallet_activity_layout.* + +class RemoveWalletActivity : BaseActivity(), RemoveWalletActivityView { + + private var authenticationResultSubject: PublishSubject? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.remove_wallet_activity_layout) + toolbar() + authenticationResultSubject = PublishSubject.create() + if (savedInstanceState == null) navigateToInitialRemoveWalletView() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == AUTHENTICATION_REQUEST_CODE) { + if (resultCode == AuthenticationPromptActivity.RESULT_OK) { + authenticationResultSubject?.onNext(true) + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + if (wallet_remove_animation == null || wallet_remove_animation.visibility != View.VISIBLE) super.onBackPressed() + return true + } + return super.onOptionsItemSelected(item) + } + + override fun onBackPressed() { + if (wallet_remove_animation == null || wallet_remove_animation.visibility != View.VISIBLE) super.onBackPressed() + } + + override fun onDestroy() { + authenticationResultSubject = null + super.onDestroy() + } + + private fun navigateToInitialRemoveWalletView() { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + RemoveWalletFragment.newInstance(walletAddress, fiatBalance)) + .commit() + } + + override fun navigateToWalletRemoveConfirmation() { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + WalletRemoveConfirmationFragment.newInstance(walletAddress, fiatBalance, + appcoinsBalance, creditsBalance, ethereumBalance)) + .addToBackStack(WalletRemoveConfirmationFragment::class.java.simpleName) + .commit() + } + + override fun navigateToWalletList() { + setResult(Activity.RESULT_OK) + finish() + } + + override fun navigateToBackUp(walletAddress: String) = + startActivity(newIntent(this, walletAddress)) + + override fun showRemoveWalletAnimation() { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED + wallet_remove_animation.visibility = View.VISIBLE + remove_wallet_animation.repeatCount = 0 + remove_wallet_animation.playAnimation() + } + + override fun showAuthentication() { + val intent = AuthenticationPromptActivity.newIntent(this) + .apply { intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP } + startActivityForResult(intent, AUTHENTICATION_REQUEST_CODE) + } + + override fun authenticationResult(): Observable { + return authenticationResultSubject!! + } + + private val walletAddress: String by lazy { + if (intent.extras!!.containsKey(WALLET_ADDRESS_KEY)) { + intent.extras!!.getString(WALLET_ADDRESS_KEY)!! + } else { + throw IllegalArgumentException("walletAddress not found") + } + } + + private val fiatBalance: String by lazy { + if (intent.extras!!.containsKey(FIAT_BALANCE_KEY)) { + intent.extras!!.getString(FIAT_BALANCE_KEY)!! + } else { + throw IllegalArgumentException("fiat balance not found") + } + } + + private val appcoinsBalance: String by lazy { + if (intent.extras!!.containsKey(APPC_BALANCE_KEY)) { + intent.extras!!.getString(APPC_BALANCE_KEY)!! + } else { + throw IllegalArgumentException("appc balance not found") + } + } + + private val creditsBalance: String by lazy { + if (intent.extras!!.containsKey(CREDITS_BALANCE_KEY)) { + intent.extras!!.getString(CREDITS_BALANCE_KEY)!! + } else { + throw IllegalArgumentException("credits balance not found") + } + } + + private val ethereumBalance: String by lazy { + if (intent.extras!!.containsKey(ETHEREUM_BALANCE_KEY)) { + intent.extras!!.getString(ETHEREUM_BALANCE_KEY)!! + } else { + throw IllegalArgumentException("ethereum balance not found") + } + } + + companion object { + private const val WALLET_ADDRESS_KEY = "wallet_address" + private const val FIAT_BALANCE_KEY = "fiat_balance" + private const val APPC_BALANCE_KEY = "appc_balance" + private const val CREDITS_BALANCE_KEY = "credits_balance" + private const val ETHEREUM_BALANCE_KEY = "ethereum_balance" + private const val AUTHENTICATION_REQUEST_CODE = 33 + + @JvmStatic + fun newIntent(context: Context, walletAddress: String, totalFiatBalance: String, + appcoinsBalance: String, creditsBalance: String, + ethereumBalance: String): Intent { + val intent = Intent(context, RemoveWalletActivity::class.java) + intent.putExtra(WALLET_ADDRESS_KEY, walletAddress) + intent.putExtra(FIAT_BALANCE_KEY, totalFiatBalance) + intent.putExtra(APPC_BALANCE_KEY, appcoinsBalance) + intent.putExtra(CREDITS_BALANCE_KEY, creditsBalance) + intent.putExtra(ETHEREUM_BALANCE_KEY, ethereumBalance) + return intent + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletActivityView.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletActivityView.kt new file mode 100644 index 00000000000..a46d91d66ec --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletActivityView.kt @@ -0,0 +1,18 @@ +package com.asfoundation.wallet.ui.wallets + +import io.reactivex.Observable + +interface RemoveWalletActivityView { + + fun navigateToWalletRemoveConfirmation() + + fun navigateToWalletList() + + fun navigateToBackUp(walletAddress: String) + + fun showRemoveWalletAnimation() + + fun showAuthentication() + + fun authenticationResult(): Observable +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletFragment.kt new file mode 100644 index 00000000000..62d15b2281f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletFragment.kt @@ -0,0 +1,96 @@ +package com.asfoundation.wallet.ui.wallets + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.remove_wallet_first_layout.* +import kotlinx.android.synthetic.main.wallet_outlined_card.* + +class RemoveWalletFragment : BasePageViewFragment(), RemoveWalletView { + + private lateinit var presenter: RemoveWalletPresenter + private lateinit var activityView: RemoveWalletActivityView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = RemoveWalletPresenter(this, + CompositeDisposable(), AndroidSchedulers.mainThread()) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is RemoveWalletActivityView) { + throw IllegalStateException( + "Remove Wallet must be attached to Remove Wallet Activity") + } + activityView = context + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.remove_wallet_first_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setWalletBalance() + presenter.present() + } + + override fun backUpWalletClick() = RxView.clicks(backup_button) + + override fun noBackUpWalletClick() = RxView.clicks(no_backup_button) + + override fun navigateToBackUp() = activityView.navigateToBackUp(walletAddress) + + override fun proceedWithRemoveWallet() = activityView.navigateToWalletRemoveConfirmation() + + private fun setWalletBalance() { + wallet_address.text = walletAddress + wallet_balance.text = fiatBalance + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + private val walletAddress: String by lazy { + if (arguments!!.containsKey(WALLET_ADDRESS_KEY)) { + arguments!!.getString(WALLET_ADDRESS_KEY)!! + } else { + throw IllegalArgumentException("walletAddress not found") + } + } + + private val fiatBalance: String by lazy { + if (arguments!!.containsKey(FIAT_BALANCE_KEY)) { + arguments!!.getString(FIAT_BALANCE_KEY)!! + } else { + throw IllegalArgumentException("fiat balance not found") + } + } + + companion object { + + private const val WALLET_ADDRESS_KEY = "wallet_address" + private const val FIAT_BALANCE_KEY = "fiat_balance" + + fun newInstance(walletAddress: String, totalFiatBalance: String): RemoveWalletFragment { + val fragment = RemoveWalletFragment() + Bundle().apply { + putString(WALLET_ADDRESS_KEY, walletAddress) + putString(FIAT_BALANCE_KEY, totalFiatBalance) + fragment.arguments = this + } + return fragment + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletPresenter.kt new file mode 100644 index 00000000000..f14250a49a9 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletPresenter.kt @@ -0,0 +1,32 @@ +package com.asfoundation.wallet.ui.wallets + +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +class RemoveWalletPresenter(private val view: RemoveWalletView, + private val disposable: CompositeDisposable, + private val viewScheduler: Scheduler) { + + fun present() { + handleBackUpClick() + handleNoBackUpClick() + } + + private fun handleBackUpClick() { + disposable.add(view.backUpWalletClick() + .observeOn(viewScheduler) + .doOnNext { view.navigateToBackUp() } + .subscribe()) + } + + private fun handleNoBackUpClick() { + disposable.add(view.noBackUpWalletClick() + .observeOn(viewScheduler) + .doOnNext { view.proceedWithRemoveWallet() } + .subscribe()) + } + + fun stop() { + disposable.clear() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletView.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletView.kt new file mode 100644 index 00000000000..6a20897bcc7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/RemoveWalletView.kt @@ -0,0 +1,11 @@ +package com.asfoundation.wallet.ui.wallets + +import io.reactivex.Observable + +interface RemoveWalletView { + + fun backUpWalletClick(): Observable + fun noBackUpWalletClick(): Observable + fun navigateToBackUp() + fun proceedWithRemoveWallet() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletBalance.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletBalance.kt new file mode 100644 index 00000000000..499e397fcbb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletBalance.kt @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.ui.wallets + +import com.asfoundation.wallet.ui.iab.FiatValue +import java.io.Serializable + +data class WalletBalance(val walletAddress: String, val balance: FiatValue, + val isActiveWallet: Boolean) : Serializable { + + constructor() : this("", FiatValue(), false) +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletDetailsFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletDetailsFragment.kt new file mode 100644 index 00000000000..9a4d8bb55e8 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletDetailsFragment.kt @@ -0,0 +1,199 @@ +package com.asfoundation.wallet.ui.wallets + +import android.annotation.SuppressLint +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.app.ShareCompat +import androidx.core.content.res.ResourcesCompat +import com.asf.wallet.R +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.ui.MyAddressActivity +import com.asfoundation.wallet.ui.balance.BalanceActivityView +import com.asfoundation.wallet.ui.balance.BalanceScreenModel +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.asfoundation.wallet.util.WalletCurrency +import com.asfoundation.wallet.util.generateQrCode +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.google.android.material.snackbar.Snackbar +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.copy_share_buttons_layout.* +import kotlinx.android.synthetic.main.fragment_wallet_details.* +import kotlinx.android.synthetic.main.remove_backup_buttons_layout.* +import kotlinx.android.synthetic.main.wallet_details_balance_layout.* +import javax.inject.Inject + +class WalletDetailsFragment : BasePageViewFragment(), WalletDetailsView { + + @Inject + lateinit var interactor: WalletDetailsInteractor + + @Inject + lateinit var walletsEventSender: WalletsEventSender + + @Inject + lateinit var currencyFormatter: CurrencyFormatUtils + private lateinit var activityView: BalanceActivityView + private lateinit var presenter: WalletDetailsPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = WalletDetailsPresenter(this, interactor, walletsEventSender, walletAddress, + CompositeDisposable(), AndroidSchedulers.mainThread(), Schedulers.io()) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is BalanceActivityView) { + throw IllegalStateException( + "Wallet Detail Fragment must be attached to Balance Activity") + } + activityView = context + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_wallet_details, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + activityView.setupToolbar() + + generateQrCode(view) + + wallet_address.text = walletAddress + + handleActiveWalletLayoutVisibility() + + presenter.present() + } + + override fun copyClick() = RxView.clicks(copy_button) + + override fun shareClick() = RxView.clicks(share_button) + + override fun removeWalletClick() = RxView.clicks(remove_button_layout) + + override fun backupInactiveWalletClick() = RxView.clicks(backup_button_layout) + + override fun backupActiveWalletClick() = RxView.clicks(middle_backup_button_layout) + + override fun makeWalletActiveClick() = RxView.clicks(make_this_active_button) + + override fun setAddressToClipBoard(walletAddress: String) { + activity?.let { + val clipboard = it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? + val clip = ClipData.newPlainText( + MyAddressActivity.KEY_ADDRESS, walletAddress) + clipboard?.setPrimaryClip(clip) + view?.let { view -> + Snackbar.make(view, R.string.wallets_address_copied_body, Snackbar.LENGTH_SHORT) + .show() + } + } + } + + override fun showShare(walletAddress: String) { + activity?.let { + ShareCompat.IntentBuilder.from(activity!!) + .setText(walletAddress) + .setType("text/plain") + .setChooserTitle(resources.getString(R.string.share_via)) + .startChooser() + } + } + + override fun navigateToBalanceView() { + activityView.showBalanceScreen() + } + + override fun navigateToBackupView(walletAddress: String) { + activityView.navigateToBackupView(walletAddress) + } + + override fun navigateToRemoveWalletView(walletAddress: String) { + activityView.navigateToRemoveWalletView(walletAddress, total_balance_fiat.text.toString(), + balance_appcoins.text.toString(), balance_credits.text.toString(), + balance_ethereum.text.toString()) + } + + @SuppressLint("SetTextI18n") + override fun populateUi(balanceScreenModel: BalanceScreenModel) { + val fiat = balanceScreenModel.overallFiat + val appc = balanceScreenModel.appcBalance.token + val credits = balanceScreenModel.creditsBalance.token + val ethereum = balanceScreenModel.ethBalance.token + total_balance_fiat.text = fiat.symbol + currencyFormatter.formatCurrency(fiat.amount) + balance_appcoins.text = + currencyFormatter.formatCurrency(appc.amount, WalletCurrency.APPCOINS) + " " + appc.symbol + balance_credits.text = currencyFormatter.formatCurrency(credits.amount, + WalletCurrency.CREDITS) + " " + credits.symbol + balance_ethereum.text = currencyFormatter.formatCurrency(ethereum.amount, + WalletCurrency.ETHEREUM) + " " + ethereum.symbol + } + + private fun handleActiveWalletLayoutVisibility() { + if (isActive) { + active_wallet_info.visibility = View.VISIBLE + middle_backup_button_layout.visibility = View.VISIBLE + } else { + remove_backup_buttons.visibility = View.VISIBLE + make_this_active_button.visibility = View.VISIBLE + } + } + + private fun generateQrCode(view: View) { + try { + val logo = ResourcesCompat.getDrawable(resources, R.drawable.ic_appc_token, null) + val mergedQrCode = walletAddress.generateQrCode(activity!!.windowManager, logo!!) + qr_image.setImageBitmap(mergedQrCode) + } catch (e: Exception) { + Snackbar.make(view, getString(R.string.error_fail_generate_qr), Snackbar.LENGTH_SHORT) + .show() + } + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + companion object { + + private const val WALLET_ADDRESS_KEY = "wallet_address" + private const val IS_ACTIVE_KEY = "is_active" + + fun newInstance(walletAddress: String, isActive: Boolean): WalletDetailsFragment { + val bundle = Bundle() + val fragment = WalletDetailsFragment() + bundle.putString(WALLET_ADDRESS_KEY, walletAddress) + bundle.putBoolean(IS_ACTIVE_KEY, isActive) + fragment.arguments = bundle + return fragment + } + } + + private val walletAddress: String by lazy { + if (arguments!!.containsKey(WALLET_ADDRESS_KEY)) { + arguments!!.getString(WALLET_ADDRESS_KEY)!! + } else { + throw IllegalArgumentException("walletAddress not found") + } + } + + private val isActive: Boolean by lazy { + if (arguments!!.containsKey(IS_ACTIVE_KEY)) { + arguments!!.getBoolean(IS_ACTIVE_KEY) + } else { + throw IllegalArgumentException("is active not found") + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletDetailsInteractor.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletDetailsInteractor.kt new file mode 100644 index 00000000000..9636c5b7ed5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletDetailsInteractor.kt @@ -0,0 +1,28 @@ +package com.asfoundation.wallet.ui.wallets + +import com.appcoins.wallet.gamification.Gamification +import com.asfoundation.wallet.interact.SetDefaultWalletInteract +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.ui.balance.BalanceInteract +import com.asfoundation.wallet.ui.balance.BalanceScreenModel +import io.reactivex.Completable +import io.reactivex.Single + +class WalletDetailsInteractor(private val balanceInteract: BalanceInteract, + private val setDefaultWalletInteract: SetDefaultWalletInteract, + private val supportInteractor: SupportInteractor, + private val gamificationRepository: Gamification) { + + fun getBalanceModel(address: String): Single = + balanceInteract.getStoredBalanceScreenModel(address) + + fun setActiveWallet(address: String): Completable { + return setDefaultWalletInteract.set(address) + .andThen(gamificationRepository.getUserStats(address)) + .flatMap { + gamificationRepository.getUserStats(address) + .doOnSuccess { supportInteractor.registerUser(it.level, address) } + } + .ignoreElement() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletDetailsPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletDetailsPresenter.kt new file mode 100644 index 00000000000..b605efbc7c5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletDetailsPresenter.kt @@ -0,0 +1,85 @@ +package com.asfoundation.wallet.ui.wallets + +import com.asfoundation.wallet.billing.analytics.WalletsAnalytics +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +class WalletDetailsPresenter( + private val view: WalletDetailsView, + private val interactor: WalletDetailsInteractor, + private val walletsEventSender: WalletsEventSender, + private val walletAddress: String, + private val disposable: CompositeDisposable, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler) { + + fun present() { + populateUi() + handleCopyClick() + handleShareClick() + handleMakeWalletActiveClick() + handleBackupClick() + handleRemoveWalletClick() + } + + private fun handleRemoveWalletClick() { + disposable.add(view.removeWalletClick() + .observeOn(viewScheduler) + .doOnNext { view.navigateToRemoveWalletView(walletAddress) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleBackupClick() { + disposable.add( + Observable.merge(view.backupInactiveWalletClick(), view.backupActiveWalletClick()) + .doOnNext { + walletsEventSender.sendCreateBackupEvent(WalletsAnalytics.ACTION_CREATE, + WalletsAnalytics.CONTEXT_WALLET_DETAILS, WalletsAnalytics.STATUS_SUCCESS) + } + .doOnError { + walletsEventSender.sendCreateBackupEvent(WalletsAnalytics.ACTION_CREATE, + WalletsAnalytics.CONTEXT_WALLET_DETAILS, WalletsAnalytics.STATUS_FAIL) + } + .observeOn(viewScheduler) + .doOnNext { view.navigateToBackupView(walletAddress) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleMakeWalletActiveClick() { + disposable.add(view.makeWalletActiveClick() + .flatMapCompletable { + interactor.setActiveWallet(walletAddress) + .observeOn(viewScheduler) + .doOnComplete { view.navigateToBalanceView() } + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleCopyClick() { + disposable.add(view.copyClick() + .observeOn(viewScheduler) + .doOnNext { view.setAddressToClipBoard(walletAddress) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleShareClick() { + disposable.add(view.shareClick() + .observeOn(viewScheduler) + .doOnNext { view.showShare(walletAddress) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun populateUi() { + disposable.add(interactor.getBalanceModel(walletAddress) + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { view.populateUi(it) } + .subscribe({}, { it.printStackTrace() })) + } + + fun stop() { + disposable.clear() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletDetailsView.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletDetailsView.kt new file mode 100644 index 00000000000..49ede033f92 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletDetailsView.kt @@ -0,0 +1,19 @@ +package com.asfoundation.wallet.ui.wallets + +import com.asfoundation.wallet.ui.balance.BalanceScreenModel +import io.reactivex.Observable + +interface WalletDetailsView { + fun populateUi(balanceScreenModel: BalanceScreenModel) + fun copyClick(): Observable + fun shareClick(): Observable + fun setAddressToClipBoard(walletAddress: String) + fun showShare(walletAddress: String) + fun makeWalletActiveClick(): Observable + fun navigateToBalanceView() + fun backupInactiveWalletClick(): Observable + fun backupActiveWalletClick(): Observable + fun removeWalletClick(): Observable + fun navigateToBackupView(walletAddress: String) + fun navigateToRemoveWalletView(walletAddress: String) +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletRemoveConfirmationFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletRemoveConfirmationFragment.kt new file mode 100644 index 00000000000..28669e9fd35 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletRemoveConfirmationFragment.kt @@ -0,0 +1,148 @@ +package com.asfoundation.wallet.ui.wallets + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.interact.DeleteWalletInteract +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.remove_wallet_balance.* +import kotlinx.android.synthetic.main.remove_wallet_second_layout.* +import javax.inject.Inject + +class WalletRemoveConfirmationFragment : BasePageViewFragment(), WalletRemoveConfirmationView { + + @Inject + lateinit var deleteWalletInteract: DeleteWalletInteract + + @Inject + lateinit var logger: Logger + private lateinit var presenter: WalletRemoveConfirmationPresenter + private lateinit var activityView: RemoveWalletActivityView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = WalletRemoveConfirmationPresenter(this, walletAddress, deleteWalletInteract, logger, + CompositeDisposable(), AndroidSchedulers.mainThread(), Schedulers.io()) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is RemoveWalletActivityView) { + throw IllegalStateException( + "Wallet Confirmation must be attached to Remove Wallet Activity") + } + activityView = context + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.remove_wallet_second_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setWalletBalance() + presenter.present() + } + + override fun noButtonClick() = RxView.clicks(no_remove_wallet_button) + + override fun yesButtonClick() = RxView.clicks(yes_remove_wallet_button) + + override fun navigateBack() { + activity?.onBackPressed() + } + + override fun showRemoveWalletAnimation() = activityView.showRemoveWalletAnimation() + + override fun finish() = activityView.navigateToWalletList() + + override fun showAuthentication() = activityView.showAuthentication() + + override fun authenticationResult() = activityView.authenticationResult() + + private fun setWalletBalance() { + wallet_address.text = walletAddress + wallet_balance.text = fiatBalance + balance_appcoins.text = appcoinsBalance + balance_credits.text = creditsBalance + balance_ethereum.text = ethereumBalance + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + private val walletAddress: String by lazy { + if (arguments!!.containsKey(WALLET_ADDRESS_KEY)) { + arguments!!.getString(WALLET_ADDRESS_KEY)!! + } else { + throw IllegalArgumentException("walletAddress not found") + } + } + + private val fiatBalance: String by lazy { + if (arguments!!.containsKey(FIAT_BALANCE_KEY)) { + arguments!!.getString(FIAT_BALANCE_KEY)!! + } else { + throw IllegalArgumentException("fiat balance not found") + } + } + + private val appcoinsBalance: String by lazy { + if (arguments!!.containsKey(APPC_BALANCE_KEY)) { + arguments!!.getString(APPC_BALANCE_KEY)!! + } else { + throw IllegalArgumentException("appc balance not found") + } + } + + private val creditsBalance: String by lazy { + if (arguments!!.containsKey(CREDITS_BALANCE_KEY)) { + arguments!!.getString(CREDITS_BALANCE_KEY)!! + } else { + throw IllegalArgumentException("credits balance not found") + } + } + + private val ethereumBalance: String by lazy { + if (arguments!!.containsKey(ETHEREUM_BALANCE_KEY)) { + arguments!!.getString(ETHEREUM_BALANCE_KEY)!! + } else { + throw IllegalArgumentException("ethereum balance not found") + } + } + + companion object { + + private const val WALLET_ADDRESS_KEY = "wallet_address" + private const val FIAT_BALANCE_KEY = "fiat_balance" + private const val APPC_BALANCE_KEY = "appc_balance" + private const val CREDITS_BALANCE_KEY = "credits_balance" + private const val ETHEREUM_BALANCE_KEY = "ethereum_balance" + + fun newInstance(walletAddress: String, totalFiatBalance: String, + appcoinsBalance: String, creditsBalance: String, + ethereumBalance: String): WalletRemoveConfirmationFragment { + val fragment = WalletRemoveConfirmationFragment() + Bundle().apply { + putString(WALLET_ADDRESS_KEY, walletAddress) + putString(FIAT_BALANCE_KEY, totalFiatBalance) + putString(APPC_BALANCE_KEY, appcoinsBalance) + putString(CREDITS_BALANCE_KEY, creditsBalance) + putString(ETHEREUM_BALANCE_KEY, ethereumBalance) + fragment.arguments = this + } + return fragment + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletRemoveConfirmationPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletRemoveConfirmationPresenter.kt new file mode 100644 index 00000000000..3d71ad8832c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletRemoveConfirmationPresenter.kt @@ -0,0 +1,78 @@ +package com.asfoundation.wallet.ui.wallets + +import com.asfoundation.wallet.interact.DeleteWalletInteract +import com.asfoundation.wallet.logging.Logger +import io.reactivex.Completable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.BiFunction +import java.util.concurrent.TimeUnit + +class WalletRemoveConfirmationPresenter(private val view: WalletRemoveConfirmationView, + private val walletAddress: String, + private val deleteWalletInteract: DeleteWalletInteract, + private val logger: Logger, + private val disposable: CompositeDisposable, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler) { + + fun present() { + handleNoButtonClick() + handleYesButtonClick() + handleAuthentication() + } + + private fun handleNoButtonClick() { + disposable.add(view.noButtonClick() + .observeOn(viewScheduler) + .doOnNext { view.navigateBack() } + .subscribe()) + } + + private fun handleYesButtonClick() { + disposable.add(view.yesButtonClick() + .throttleFirst(100, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .flatMapSingle { + var authenticationRequired = false + if (deleteWalletInteract.hasAuthenticationPermission()) { + view.showAuthentication() + authenticationRequired = true + } else { + view.showRemoveWalletAnimation() + } + Single.just(authenticationRequired) + } + .filter { authenticationRequired -> !authenticationRequired } + .observeOn(networkScheduler) + .flatMapSingle { deleteWallet() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleAuthentication() { + disposable.add(view.authenticationResult() + .filter { it } + .observeOn(viewScheduler) + .doOnNext { view.showRemoveWalletAnimation() } + .observeOn(networkScheduler) + .flatMapSingle { deleteWallet() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun deleteWallet(): Single { + return Single.zip(deleteWalletInteract.delete(walletAddress) + .toSingleDefault(Unit), + Completable.timer(2, TimeUnit.SECONDS) + .toSingleDefault(Unit), + BiFunction { _: Unit, _: Unit -> }) + .observeOn(viewScheduler) + .doOnSuccess { view.finish() } + .doOnError { + logger.log("WalletRemoveConfirmationPresenter", it) + view.finish() + } + } + + fun stop() = disposable.clear() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletRemoveConfirmationView.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletRemoveConfirmationView.kt new file mode 100644 index 00000000000..8741afeabaa --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletRemoveConfirmationView.kt @@ -0,0 +1,20 @@ +package com.asfoundation.wallet.ui.wallets + +import io.reactivex.Observable + +interface WalletRemoveConfirmationView { + + fun noButtonClick(): Observable + + fun yesButtonClick(): Observable + + fun navigateBack() + + fun showRemoveWalletAnimation() + + fun finish() + + fun showAuthentication() + + fun authenticationResult(): Observable +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsAdapter.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsAdapter.kt new file mode 100644 index 00000000000..230f4363c7f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsAdapter.kt @@ -0,0 +1,35 @@ +package com.asfoundation.wallet.ui.wallets + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R +import com.asfoundation.wallet.util.CurrencyFormatUtils +import io.reactivex.subjects.PublishSubject + +class WalletsAdapter(private val context: Context, private var items: List, + private val uiEventListener: PublishSubject, + private val currencyFormatUtils: CurrencyFormatUtils, + private val walletsViewType: WalletsViewType) : + RecyclerView.Adapter() { + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WalletsViewHolder { + val view = when (walletsViewType) { + WalletsViewType.BALANCE -> LayoutInflater.from(parent.context) + .inflate(R.layout.other_wallet_card, parent, false) + WalletsViewType.SETTINGS -> LayoutInflater.from(parent.context) + .inflate(R.layout.wallet_rounded_outlined_card, parent, false) + } + return WalletsViewHolder(context, view, uiEventListener, currencyFormatUtils, walletsViewType) + } + + override fun getItemCount(): Int { + return items.size + } + + override fun onBindViewHolder(holder: WalletsViewHolder, position: Int) { + holder.bind(items[position]) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsFragment.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsFragment.kt new file mode 100644 index 00000000000..398806c4e7c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsFragment.kt @@ -0,0 +1,160 @@ +package com.asfoundation.wallet.ui.wallets + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.ui.balance.BalanceActivityView +import com.asfoundation.wallet.ui.balance.BalanceFragmentView +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.util.CurrencyFormatUtils +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.active_wallet_card.* +import kotlinx.android.synthetic.main.active_wallet_card.view.* +import kotlinx.android.synthetic.main.fragment_wallets_bottom_sheet.* +import kotlinx.android.synthetic.main.restore_create_buttons_layout.* +import javax.inject.Inject + +class WalletsFragment : DaggerFragment(), WalletsView { + + @Inject + lateinit var walletsInteract: WalletsInteract + + @Inject + lateinit var currencyFormatter: CurrencyFormatUtils + + @Inject + lateinit var logger: Logger + private var uiEventListener: PublishSubject? = null + private var onBackPressSubject: PublishSubject? = null + private lateinit var activityView: BalanceActivityView + private lateinit var adapter: WalletsAdapter + private lateinit var presenter: WalletsPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + uiEventListener = PublishSubject.create() + onBackPressSubject = PublishSubject.create() + presenter = WalletsPresenter(this, walletsInteract, logger, CompositeDisposable(), + AndroidSchedulers.mainThread(), Schedulers.io()) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is BalanceActivityView) { + throw IllegalStateException( + "Wallets Fragment must be attached to Balance Activity") + } + activityView = context + } + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_wallets_bottom_sheet, container, false) + } + + @SuppressLint("SetTextI18n") + override fun setupUi(totalWallets: Int, totalBalance: FiatValue, + walletsBalanceList: List) { + total_wallets.text = totalWallets.toString() + total_wallets.visibility = View.VISIBLE + wallets_skeleton.visibility = View.GONE + accumulated_value.text = + totalBalance.symbol + currencyFormatter.formatCurrency(totalBalance.amount) + accumulated_value.visibility = View.VISIBLE + accumulated_value_skeleton.visibility = View.GONE + + val currentWalletBalance = getCurrentWalletBalance(walletsBalanceList) + active_wallet_address.text = currentWalletBalance.walletAddress + active_wallet_card.wallet_balance.text = getString( + R.string.wallets_2nd_view_balance_title) + " " + totalBalance.symbol + + currencyFormatter.formatCurrency(currentWalletBalance.balance.amount) + + + val adapterList = removeCurrentWallet(walletsBalanceList) + adapter = + WalletsAdapter(context!!, adapterList, uiEventListener!!, currencyFormatter, + WalletsViewType.BALANCE) + other_wallets_cards_recycler.adapter = adapter + val walletsText = + resources.getQuantityString(R.plurals.wallets_bottom_wallets_title, walletsBalanceList.size) + wallets_text.text = walletsText + if (adapterList.isEmpty()) other_wallets_header.visibility = View.INVISIBLE + else other_wallets_header.visibility = View.VISIBLE + } + + override fun otherWalletCardClicked() = uiEventListener!! + + override fun activeWalletCardClicked(): Observable = RxView.clicks(active_wallet_card) + .map { active_wallet_address.text.toString() } + + override fun restoreWalletClicked(): Observable = RxView.clicks(restore_button_layout) + + override fun createNewWalletClicked(): Observable = RxView.clicks(create_new_button_layout) + + override fun navigateToRestoreView() = activityView.navigateToRestoreView() + + override fun showCreatingAnimation() { + val parentFragment = provideParentFragment() + parentFragment?.showCreatingAnimation() + } + + override fun showWalletCreatedAnimation() { + val parentFragment = provideParentFragment() + parentFragment?.showWalletCreatedAnimation() + } + + override fun navigateToWalletDetailView(walletAddress: String, isActive: Boolean) = + activityView.navigateToWalletDetailView(walletAddress, isActive) + + override fun onBottomSheetHeaderClicked() = RxView.clicks(bottom_sheet_header) + + override fun changeBottomSheetState() { + val parentFragment = provideParentFragment() + parentFragment?.changeBottomSheetState() + } + + private fun removeCurrentWallet(walletsBalanceList: List): List { + val otherWalletsBalanceList = ArrayList() + for (balance in walletsBalanceList) { + if (!balance.isActiveWallet) otherWalletsBalanceList.add(balance) + } + return otherWalletsBalanceList + } + + private fun getCurrentWalletBalance( + walletBalanceList: List): WalletBalance { + for (walletBalance in walletBalanceList) { + if (walletBalance.isActiveWallet) return walletBalance + } + return WalletBalance() + } + + private fun provideParentFragment(): BalanceFragmentView? { + if (parentFragment !is BalanceFragmentView) { + return null + } + return parentFragment as BalanceFragmentView + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsInteract.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsInteract.kt new file mode 100644 index 00000000000..a83571793ab --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsInteract.kt @@ -0,0 +1,68 @@ +package com.asfoundation.wallet.ui.wallets + +import com.appcoins.wallet.gamification.Gamification +import com.asfoundation.wallet.entity.Wallet +import com.asfoundation.wallet.interact.FetchWalletsInteract +import com.asfoundation.wallet.interact.WalletCreatorInteract +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.repository.SharedPreferencesRepository +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.ui.balance.BalanceInteract +import com.asfoundation.wallet.ui.iab.FiatValue +import com.asfoundation.wallet.util.sumByBigDecimal +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single + +class WalletsInteract(private val balanceInteract: BalanceInteract, + private val fetchWalletsInteract: FetchWalletsInteract, + private val walletCreatorInteract: WalletCreatorInteract, + private val supportInteractor: SupportInteractor, + private val preferencesRepository: SharedPreferencesRepository, + private val gamificationRepository: Gamification, + private val logger: Logger) { + + fun retrieveWalletsModel(): Single { + val wallets = ArrayList() + val currentWalletAddress = preferencesRepository.getCurrentWalletAddress() + return retrieveWallets().filter { it.isNotEmpty() } + .flatMapCompletable { list -> + Observable.fromIterable(list) + .flatMapCompletable { wallet -> + balanceInteract.getTotalBalance(wallet.address) + .firstOrError() + .doOnSuccess { fiatValue -> + wallets.add(WalletBalance(wallet.address, fiatValue, + currentWalletAddress == wallet.address)) + } + .doOnError { logger.log("WalletsInteract", it) } + .ignoreElement() + } + } + .toSingle { + WalletsModel(getTotalBalance(wallets), wallets.size, wallets) + } + } + + fun createWallet(): Completable { + return walletCreatorInteract.create() + .flatMapCompletable { wallet -> + walletCreatorInteract.setDefaultWallet(wallet.address) + .andThen(gamificationRepository.getUserStats(wallet.address) + .doOnSuccess { supportInteractor.registerUser(it.level, wallet.address) } + .toCompletable()) + } + } + + private fun getTotalBalance(walletBalance: List): FiatValue { + val totalBalance = walletBalance.sumByBigDecimal { it.balance.amount } + return FiatValue(totalBalance, walletBalance[0].balance.currency, + walletBalance[0].balance.symbol) + } + + private fun retrieveWallets(): Observable> { + return fetchWalletsInteract.fetch() + .map { it.toList() } + .toObservable() + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsModel.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsModel.kt new file mode 100644 index 00000000000..49780d31794 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsModel.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.ui.wallets + +import com.asfoundation.wallet.ui.iab.FiatValue +import java.io.Serializable + +data class WalletsModel(val totalBalance: FiatValue, val totalWallets: Int, + val walletsBalance: List) : Serializable diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsPresenter.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsPresenter.kt new file mode 100644 index 00000000000..eba2b8f52bb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsPresenter.kt @@ -0,0 +1,75 @@ +package com.asfoundation.wallet.ui.wallets + +import com.asfoundation.wallet.logging.Logger +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.TimeUnit + +class WalletsPresenter(private val view: WalletsView, + private val walletsInteract: WalletsInteract, + private val logger: Logger, + private val disposables: CompositeDisposable, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler) { + fun present() { + retrieveViewInformation() + handleActiveWalletCardClick() + handleOtherWalletCardClick() + handleCreateNewWalletClick() + handleRestoreWalletClick() + handleBottomSheetHeaderClick() + } + + private fun handleBottomSheetHeaderClick() { + disposables.add(view.onBottomSheetHeaderClicked() + .doOnNext { view.changeBottomSheetState() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleRestoreWalletClick() { + disposables.add(view.restoreWalletClicked() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .doOnNext { view.navigateToRestoreView() } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleCreateNewWalletClick() { + disposables.add(view.createNewWalletClicked() + .throttleFirst(100, TimeUnit.MILLISECONDS) + .doOnNext { view.showCreatingAnimation() } + .observeOn(networkScheduler) + .flatMapCompletable { + walletsInteract.createWallet() + .observeOn(viewScheduler) + .andThen { view.showWalletCreatedAnimation() } + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleOtherWalletCardClick() { + disposables.add(view.otherWalletCardClicked() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .doOnNext { view.navigateToWalletDetailView(it, false) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleActiveWalletCardClick() { + disposables.add(view.activeWalletCardClicked() + .throttleFirst(50, TimeUnit.MILLISECONDS) + .observeOn(viewScheduler) + .doOnNext { view.navigateToWalletDetailView(it, true) } + .subscribe({}, { it.printStackTrace() })) + } + + private fun retrieveViewInformation() { + disposables.add(walletsInteract.retrieveWalletsModel() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { view.setupUi(it.totalWallets, it.totalBalance, it.walletsBalance) } + .subscribe({}, { logger.log("WalletsPresenter", it) })) + } + + fun stop() = disposables.clear() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsView.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsView.kt new file mode 100644 index 00000000000..483c6207d83 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsView.kt @@ -0,0 +1,21 @@ +package com.asfoundation.wallet.ui.wallets + +import com.asfoundation.wallet.ui.iab.FiatValue +import io.reactivex.Observable + +interface WalletsView { + + fun setupUi(totalWallets: Int, totalBalance: FiatValue, + walletsBalanceList: List) + + fun otherWalletCardClicked(): Observable + fun activeWalletCardClicked(): Observable + fun navigateToWalletDetailView(walletAddress: String, isActive: Boolean) + fun createNewWalletClicked(): Observable + fun showCreatingAnimation() + fun showWalletCreatedAnimation() + fun restoreWalletClicked(): Observable + fun navigateToRestoreView() + fun onBottomSheetHeaderClicked(): Observable + fun changeBottomSheetState() +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsViewHolder.kt new file mode 100644 index 00000000000..281990ab713 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsViewHolder.kt @@ -0,0 +1,34 @@ +package com.asfoundation.wallet.ui.wallets + +import android.annotation.SuppressLint +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R +import com.asfoundation.wallet.util.CurrencyFormatUtils +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.other_wallet_card.view.* +import kotlinx.android.synthetic.main.other_wallet_card.view.wallet_balance +import kotlinx.android.synthetic.main.wallet_rounded_outlined_card.view.* + +class WalletsViewHolder(private val context: Context, itemView: View, + private val uiEventListener: PublishSubject, + private val currencyFormatUtils: CurrencyFormatUtils, + private val walletsViewType: WalletsViewType) : + RecyclerView.ViewHolder(itemView) { + + @SuppressLint("SetTextI18n") + fun bind(item: WalletBalance) { + if (walletsViewType == WalletsViewType.BALANCE) { + itemView.inactive_wallet_address.text = item.walletAddress + itemView.wallet_balance.text = context.getString( + R.string.wallets_2nd_view_balance_title) + " " + item.balance.symbol + currencyFormatUtils.formatCurrency( + item.balance.amount) + } else if (walletsViewType == WalletsViewType.SETTINGS) { + itemView.wallet_address.text = item.walletAddress + itemView.wallet_balance.text = + "${item.balance.symbol}${currencyFormatUtils.formatCurrency(item.balance.amount)}" + } + itemView.setOnClickListener { uiEventListener.onNext(item.walletAddress) } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsViewType.kt b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsViewType.kt new file mode 100644 index 00000000000..d52a1934a7f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/wallets/WalletsViewType.kt @@ -0,0 +1,5 @@ +package com.asfoundation.wallet.ui.wallets + +enum class WalletsViewType { + BALANCE, SETTINGS +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/AutoFitEditText.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/AutoFitEditText.java new file mode 100644 index 00000000000..19ae274be3c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/AutoFitEditText.java @@ -0,0 +1,257 @@ +package com.asfoundation.wallet.ui.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.os.Build; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.util.SparseIntArray; +import android.util.TypedValue; +import androidx.appcompat.widget.AppCompatEditText; + +public class AutoFitEditText extends AppCompatEditText { + private static final int NO_LINE_LIMIT = -1; + private final RectF _availableSpaceRect = new RectF(); + private final SparseIntArray _textCachedSizes = new SparseIntArray(); + private final SizeTester _sizeTester; + private float _maxTextSize; + private float _spacingMult = 1.0f; + private float _spacingAdd = 0.0f; + private Float _minTextSize; + private int _widthLimit; + private int _maxLines; + private boolean _enableSizeCache = true; + private boolean _initialized; + private TextPaint paint; + + public AutoFitEditText(final Context context) { + this(context, null, 0); + } + + public AutoFitEditText(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public AutoFitEditText(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + // using the minimal recommended font size + _minTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, + getResources().getDisplayMetrics()); + _maxTextSize = getTextSize(); + if (_maxLines == 0) + // no value was assigned during construction + { + _maxLines = NO_LINE_LIMIT; + } + // prepare size tester: + _sizeTester = new SizeTester() { + final RectF textRect = new RectF(); + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override + public int onTestSize(final int suggestedSize, final RectF availableSPace) { + paint.setTextSize(suggestedSize); + final String text = getText().toString(); + final boolean singleline = getMaxLines() == 1; + if (singleline) { + textRect.bottom = paint.getFontSpacing(); + textRect.right = paint.measureText(text); + } else { + final StaticLayout layout = + new StaticLayout(text, paint, _widthLimit, Layout.Alignment.ALIGN_NORMAL, + _spacingMult, _spacingAdd, true); + if (getMaxLines() != NO_LINE_LIMIT && layout.getLineCount() > getMaxLines()) return 1; + textRect.bottom = layout.getHeight(); + int maxWidth = -1; + for (int i = 0; i < layout.getLineCount(); i++) + if (maxWidth < layout.getLineWidth(i)) maxWidth = (int) layout.getLineWidth(i); + textRect.right = maxWidth; + } + textRect.offsetTo(0, 0); + if (availableSPace.contains(textRect)) + // may be too small, don't worry we will find the best match + { + return -1; + } + // else, too big + return 1; + } + }; + _initialized = true; + } + + @Override public void setTextSize(final float size) { + _maxTextSize = size; + _textCachedSizes.clear(); + adjustTextSize(); + } + + @Override public void setTextSize(final int unit, final float size) { + final Context c = getContext(); + Resources r; + if (c == null) { + r = Resources.getSystem(); + } else { + r = c.getResources(); + } + _maxTextSize = TypedValue.applyDimension(unit, size, r.getDisplayMetrics()); + _textCachedSizes.clear(); + adjustTextSize(); + } + + @Override public void setTypeface(final Typeface tf) { + if (paint == null) paint = new TextPaint(getPaint()); + paint.setTypeface(tf); + super.setTypeface(tf); + } + + /** + * Set the lower text size limit and invalidate the view + * + * @param + */ + public void setMinTextSize(final Float minTextSize) { + _minTextSize = minTextSize; + reAdjust(); + } + + public Float get_minTextSize() { + return _minTextSize; + } @Override public void setMaxLines(final int maxlines) { + super.setMaxLines(maxlines); + _maxLines = maxlines; + reAdjust(); + } + + private void reAdjust() { + adjustTextSize(); + } + + private void adjustTextSize() { + if (!_initialized) return; + final int startSize = Math.round(_minTextSize); + final int heightLimit = + getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop(); + _widthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(); + if (_widthLimit <= 0) return; + _availableSpaceRect.right = _widthLimit; + _availableSpaceRect.bottom = heightLimit; + super.setTextSize(TypedValue.COMPLEX_UNIT_PX, + efficientTextSizeSearch(startSize, (int) _maxTextSize, _sizeTester, _availableSpaceRect)); + } @Override public int getMaxLines() { + return _maxLines; + } + + /** + * Enables or disables size caching, enabling it will improve performance + * where you are animating a value inside TextView. This stores the font + * size against getText().length() Be careful though while enabling it as 0 + * takes more space than 1 on some fonts and so on. + * + * @param enable enable font size caching + */ + public void setEnableSizeCache(final boolean enable) { + _enableSizeCache = enable; + _textCachedSizes.clear(); + adjustTextSize(); + } + + private int efficientTextSizeSearch(final int start, final int end, final SizeTester sizeTester, + final RectF availableSpace) { + if (!_enableSizeCache) return binarySearch(start, end, sizeTester, availableSpace); + final String text = getText().toString(); + final int key = text == null ? 0 : text.length(); + int size = _textCachedSizes.get(key); + if (size != 0) return size; + size = binarySearch(start, end, sizeTester, availableSpace); + _textCachedSizes.put(key, size); + return size; + } @Override public void setSingleLine() { + super.setSingleLine(); + _maxLines = 1; + reAdjust(); + } + + private int binarySearch(final int start, final int end, final SizeTester sizeTester, + final RectF availableSpace) { + int lastBest = start; + int lo = start; + int hi = end - 1; + int mid = 0; + while (lo <= hi) { + mid = lo + hi >>> 1; + final int midValCmp = sizeTester.onTestSize(mid, availableSpace); + if (midValCmp < 0) { + lastBest = lo; + lo = mid + 1; + } else if (midValCmp > 0) { + hi = mid - 1; + lastBest = hi; + } else { + return mid; + } + } + // make sure to return last best + // this is what should always be returned + return lastBest; + } + + @Override protected void onSizeChanged(final int width, final int height, final int oldwidth, + final int oldheight) { + _textCachedSizes.clear(); + super.onSizeChanged(width, height, oldwidth, oldheight); + if (width != oldwidth || height != oldheight) reAdjust(); + } @Override public void setSingleLine(final boolean singleLine) { + super.setSingleLine(singleLine); + if (singleLine) { + _maxLines = 1; + } else { + _maxLines = NO_LINE_LIMIT; + } + reAdjust(); + } + + private interface SizeTester { + /** + * AutoFitEditText + * + * @param suggestedSize Size of text to be tested + * @param availableSpace available space in which text must fit + * + * @return an integer < 0 if after applying {@code suggestedSize} to + * text, it takes less space than {@code availableSpace}, > 0 + * otherwise + */ + int onTestSize(int suggestedSize, RectF availableSpace); + } + + @Override public void setLines(final int lines) { + super.setLines(lines); + _maxLines = lines; + reAdjust(); + } + + + + + + @Override public void setLineSpacing(final float add, final float mult) { + super.setLineSpacing(add, mult); + _spacingMult = mult; + _spacingAdd = add; + } + + + + + + @Override protected void onTextChanged(final CharSequence text, final int start, final int before, + final int after) { + super.onTextChanged(text, start, before, after); + reAdjust(); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/MarginItemDecoration.kt b/app/src/main/java/com/asfoundation/wallet/ui/widget/MarginItemDecoration.kt new file mode 100644 index 00000000000..47aae5731b5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/MarginItemDecoration.kt @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.ui.widget + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +class MarginItemDecoration(private val itemMargin: Int) : RecyclerView.ItemDecoration() { + override fun getItemOffsets(outRect: Rect, view: View, + parent: RecyclerView, state: RecyclerView.State) { + with(outRect) { + bottom = itemMargin + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnBackupClickListener.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/OnBackupClickListener.java deleted file mode 100644 index 508ee22c0d9..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnBackupClickListener.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.asfoundation.wallet.ui.widget; - -import android.view.View; -import com.asfoundation.wallet.entity.Wallet; - -public interface OnBackupClickListener { - void onBackupClick(View view, Wallet wallet); -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnDepositClickListener.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/OnDepositClickListener.java deleted file mode 100644 index 7d29990515d..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnDepositClickListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.asfoundation.wallet.ui.widget; - -import android.net.Uri; -import android.view.View; - -public interface OnDepositClickListener { - - void onDepositClick(View view, Uri uri); -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnImportKeystoreListener.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/OnImportKeystoreListener.java deleted file mode 100644 index ebcf4a9b317..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnImportKeystoreListener.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.asfoundation.wallet.ui.widget; - -public interface OnImportKeystoreListener { - void onKeystore(String keystore, String password); -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnImportPrivateKeyListener.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/OnImportPrivateKeyListener.java deleted file mode 100644 index 6b47672d454..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnImportPrivateKeyListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.asfoundation.wallet.ui.widget; - -public interface OnImportPrivateKeyListener { - - void onPrivateKey(String key); -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnMoreClickListener.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/OnMoreClickListener.java index 656efe58d14..2078e1af0fc 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnMoreClickListener.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/OnMoreClickListener.java @@ -2,7 +2,6 @@ import android.view.View; import com.asfoundation.wallet.transactions.Operation; -import com.asfoundation.wallet.transactions.Transaction; public interface OnMoreClickListener { void onTransactionClick(View view, Operation operation); diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnTokenClickListener.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/OnTokenClickListener.java deleted file mode 100644 index 7ce554a46b8..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnTokenClickListener.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.asfoundation.wallet.ui.widget; - -import android.view.View; -import com.asfoundation.wallet.entity.Token; - -public interface OnTokenClickListener { - void onTokenClick(View view, Token token); -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnTransactionClickListener.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/OnTransactionClickListener.java index cd9f55ebd9e..8d190087bf1 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/OnTransactionClickListener.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/OnTransactionClickListener.java @@ -1,7 +1,6 @@ package com.asfoundation.wallet.ui.widget; import android.view.View; -import com.asfoundation.wallet.entity.RawTransaction; import com.asfoundation.wallet.transactions.Transaction; public interface OnTransactionClickListener { diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/ApplicationSortedItem.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/ApplicationSortedItem.java new file mode 100644 index 00000000000..a5141c66bcb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/ApplicationSortedItem.java @@ -0,0 +1,23 @@ +package com.asfoundation.wallet.ui.widget.adapter; + +import com.asfoundation.wallet.ui.appcoins.applications.AppcoinsApplication; +import com.asfoundation.wallet.ui.widget.entity.SortedItem; +import java.util.List; + +class ApplicationSortedItem extends SortedItem> { + public ApplicationSortedItem(List value, int viewType) { + super(viewType, value, Integer.MIN_VALUE); + } + + @Override public int compare(SortedItem other) { + return weight - other.weight; + } + + @Override public boolean areContentsTheSame(SortedItem newItem) { + return viewType == newItem.viewType && value.equals(newItem.value); + } + + @Override public boolean areItemsTheSame(SortedItem other) { + return viewType == other.viewType; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/CardNotificationSortedItem.kt b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/CardNotificationSortedItem.kt new file mode 100644 index 00000000000..aa0795100cc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/CardNotificationSortedItem.kt @@ -0,0 +1,20 @@ +package com.asfoundation.wallet.ui.widget.adapter + +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.ui.widget.entity.SortedItem + +class CardNotificationSortedItem(value: List, viewType: Int) : + SortedItem>(viewType, value, Integer.MIN_VALUE) { + + override fun compare(other: SortedItem<*>): Int { + return weight - other.weight + } + + override fun areContentsTheSame(newItem: SortedItem<*>): Boolean { + return viewType == newItem.viewType && value == newItem.value + } + + override fun areItemsTheSame(other: SortedItem<*>): Boolean { + return viewType == other.viewType + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/CardNotificationsAdapter.kt b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/CardNotificationsAdapter.kt new file mode 100644 index 00000000000..e343eb0b2ee --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/CardNotificationsAdapter.kt @@ -0,0 +1,62 @@ +package com.asfoundation.wallet.ui.widget.adapter + +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction +import com.asfoundation.wallet.ui.widget.holder.CardNotificationViewHolder +import rx.functions.Action2 + +class CardNotificationsAdapter( + var notifications: List, + private val listener: Action2 +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, + viewType: Int): CardNotificationViewHolder { + + val item = LayoutInflater.from(parent.context) + .inflate(R.layout.item_card_notification, parent, + false) + + val maxWith = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 400f, + parent.context.resources + .displayMetrics) + .toInt() + + val margins = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32f, + parent.context.resources + .displayMetrics) + .toInt() + + if (itemCount > 1) { + val screenWith = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, parent.measuredWidth.toFloat(), + parent.context.resources + .displayMetrics) + .toInt() + + if (screenWith > maxWith) { + val lp = item.layoutParams as ViewGroup.LayoutParams + lp.width = maxWith + item.layoutParams = lp + } else { + val lp = item.layoutParams as ViewGroup.LayoutParams + lp.width = screenWith - margins + item.layoutParams = lp + } + } + + return CardNotificationViewHolder(item, listener) + } + + override fun getItemCount() = notifications.size + + override fun onBindViewHolder(holder: CardNotificationViewHolder, position: Int) { + holder.bind(notifications[position]) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/ChangeTokenCollectionAdapter.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/ChangeTokenCollectionAdapter.java deleted file mode 100644 index 65db25b934f..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/ChangeTokenCollectionAdapter.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.asfoundation.wallet.ui.widget.adapter; - -import android.support.v7.widget.RecyclerView; -import android.view.ViewGroup; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.ui.widget.OnTokenClickListener; -import com.asfoundation.wallet.ui.widget.holder.TokenChangeHolder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class ChangeTokenCollectionAdapter extends RecyclerView.Adapter { - private final List items = new ArrayList<>(); - - private final OnTokenClickListener onTokenClickListener; - private final OnTokenClickListener onTokenDeleteClickListener; - - public ChangeTokenCollectionAdapter(OnTokenClickListener onTokenClickListener, - OnTokenClickListener onTokenDeleteClickListener) { - this.onTokenClickListener = onTokenClickListener; - this.onTokenDeleteClickListener = onTokenDeleteClickListener; - } - - @Override public TokenChangeHolder onCreateViewHolder(ViewGroup parent, int viewType) { - TokenChangeHolder tokenHolder = new TokenChangeHolder(R.layout.item_change_token, parent); - tokenHolder.setOnTokenClickListener(onTokenClickListener); - tokenHolder.setOnTokenDeleteClickListener(onTokenDeleteClickListener); - return tokenHolder; - } - - @Override public void onBindViewHolder(TokenChangeHolder holder, int position) { - holder.bind(items.get(position)); - } - - @Override public int getItemCount() { - return items.size(); - } - - public void setTokens(Token[] tokens) { - items.clear(); - items.addAll(Arrays.asList(tokens)); - notifyDataSetChanged(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/EmptyTransactionPagerAdapter.kt b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/EmptyTransactionPagerAdapter.kt new file mode 100644 index 00000000000..d96a891a5d3 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/EmptyTransactionPagerAdapter.kt @@ -0,0 +1,100 @@ +package com.asfoundation.wallet.ui.widget.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.viewpager.widget.PagerAdapter +import androidx.viewpager.widget.ViewPager +import com.airbnb.lottie.LottieAnimationView +import com.asf.wallet.R +import io.reactivex.subjects.PublishSubject +import org.jetbrains.annotations.NotNull + +class EmptyTransactionPagerAdapter(private val animation: IntArray, + private val body: Array<@NotNull String>, + private val action: IntArray, + private val numberPages: Int, + private val viewPager: @NotNull ViewPager, + private val emptyTransactionsSubject: PublishSubject) : + PagerAdapter() { + + override fun getCount(): Int { + return numberPages + } + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + val view = LayoutInflater.from(container.context) + .inflate(R.layout.layout_empty_transactions_viewpager, container, false) + (view.findViewById( + R.id.transactions_empty_screen_animation) as LottieAnimationView).setAnimation( + animation[position]) + (view.findViewById(R.id.empty_body_text) as TextView).text = body[position] + (view.findViewById(R.id.empty_action_text) as TextView).setText(action[position]) + + viewPager.addOnPageChangeListener(EmptyTransactionsPageChangeListener(view)) + + when (action[position]) { + R.string.home_empty_discover_apps_button -> { + (view.findViewById(R.id.empty_action_text) as TextView).setOnClickListener { + emptyTransactionsSubject.onNext(CAROUSEL_TOP_APPS) + } + (view.findViewById( + R.id.transactions_empty_screen_animation) as LottieAnimationView).setOnClickListener { + emptyTransactionsSubject.onNext(CAROUSEL_TOP_APPS) + } + } + R.string.gamification_home_button -> { + (view.findViewById(R.id.empty_action_text) as TextView).setOnClickListener { + emptyTransactionsSubject.onNext(CAROUSEL_GAMIFICATION) + } + (view.findViewById( + R.id.transactions_empty_screen_animation) as LottieAnimationView).setOnClickListener { + emptyTransactionsSubject.onNext(CAROUSEL_GAMIFICATION) + } + } + } + container.addView(view) + return view + } + + override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { + container.removeView(`object` as View) + } + + override fun isViewFromObject(view: View, `object`: Any): Boolean { + return view === `object` + } + + fun randomizeCarouselContent() { + val randomIndex = Math.random() + if (randomIndex >= 0.5) { + invertAnimationContent(animation) + invertBodyContent(body) + invertActionContent(action) + } + } + + private fun invertAnimationContent(animContent: IntArray) { + val tempAnim = animContent[0] + animContent[0] = animContent[1] + animContent[1] = tempAnim + } + + private fun invertBodyContent(bodyContent: Array<@NotNull String>) { + val tempBody = bodyContent[0] + bodyContent[0] = bodyContent[1] + bodyContent[1] = tempBody + } + + private fun invertActionContent(actionContent: IntArray) { + val tempAction = actionContent[0] + actionContent[0] = actionContent[1] + actionContent[1] = tempAction + } + + companion object { + const val CAROUSEL_TOP_APPS: String = "bundle" + const val CAROUSEL_GAMIFICATION: String = "gamification" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/EmptyTransactionsPageChangeListener.kt b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/EmptyTransactionsPageChangeListener.kt new file mode 100644 index 00000000000..861c54e4298 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/EmptyTransactionsPageChangeListener.kt @@ -0,0 +1,33 @@ +package com.asfoundation.wallet.ui.widget.adapter + +import android.view.View +import androidx.viewpager.widget.ViewPager +import com.airbnb.lottie.LottieAnimationView +import com.asf.wallet.R + + +class EmptyTransactionsPageChangeListener(view: View) : ViewPager.OnPageChangeListener { + + private var lottieView: LottieAnimationView = + view.findViewById(R.id.transactions_empty_screen_animation) + private var isAnimationPlaying: Boolean = false + + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + if (positionOffset.compareTo(0.0) != 0) { + if (isAnimationPlaying) { + lottieView.cancelAnimation() + isAnimationPlaying = false + } + } else if (!isAnimationPlaying) { + isAnimationPlaying = true + lottieView.resumeAnimation() + } + } + + override fun onPageScrollStateChanged(state: Int) { + } + + override fun onPageSelected(position: Int) { + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TabPagerAdapter.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TabPagerAdapter.java deleted file mode 100644 index 6636f80accb..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TabPagerAdapter.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.asfoundation.wallet.ui.widget.adapter; - -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.util.Pair; -import java.util.List; - -public class TabPagerAdapter extends FragmentPagerAdapter { - - private final List> pages; - - public TabPagerAdapter(FragmentManager fm, List> pages) { - super(fm); - - this.pages = pages; - } - - // Return fragment with respect to position. - @Override public Fragment getItem(int position) { - return pages.get(position).second; - } - - @Override public int getCount() { - return pages.size(); - } - - // This method returns the title of the tab according to its position. - @Override public CharSequence getPageTitle(int position) { - return pages.get(position).first; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TokensAdapter.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TokensAdapter.java deleted file mode 100644 index 32e2ea84708..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TokensAdapter.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.asfoundation.wallet.ui.widget.adapter; - -import android.support.v7.util.SortedList; -import android.support.v7.widget.RecyclerView; -import android.view.ViewGroup; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.ui.widget.OnTokenClickListener; -import com.asfoundation.wallet.ui.widget.entity.SortedItem; -import com.asfoundation.wallet.ui.widget.entity.TokenSortedItem; -import com.asfoundation.wallet.ui.widget.entity.TotalBalanceSortedItem; -import com.asfoundation.wallet.ui.widget.holder.BinderViewHolder; -import com.asfoundation.wallet.ui.widget.holder.TokenHolder; -import com.asfoundation.wallet.ui.widget.holder.TotalBalanceHolder; -import java.math.BigDecimal; - -public class TokensAdapter extends RecyclerView.Adapter { - - private final OnTokenClickListener onTokenClickListener; - private final SortedList items = - new SortedList<>(SortedItem.class, new SortedList.Callback() { - @Override public int compare(SortedItem o1, SortedItem o2) { - return o1.compare(o2); - } - - @Override public void onChanged(int position, int count) { - notifyItemRangeChanged(position, count); - } - - @Override public boolean areContentsTheSame(SortedItem oldItem, SortedItem newItem) { - return oldItem.areContentsTheSame(newItem); - } - - @Override public boolean areItemsTheSame(SortedItem item1, SortedItem item2) { - return item1.areItemsTheSame(item2); - } - - @Override public void onInserted(int position, int count) { - notifyItemRangeInserted(position, count); - } - - @Override public void onRemoved(int position, int count) { - notifyItemRangeRemoved(position, count); - } - - @Override public void onMoved(int fromPosition, int toPosition) { - notifyItemMoved(fromPosition, toPosition); - } - }); - private TotalBalanceSortedItem total = new TotalBalanceSortedItem(null); - - public TokensAdapter(OnTokenClickListener onTokenClickListener) { - this.onTokenClickListener = onTokenClickListener; - } - - @Override public BinderViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - BinderViewHolder holder = null; - switch (viewType) { - case TokenHolder.VIEW_TYPE: { - TokenHolder tokenHolder = new TokenHolder(R.layout.item_token, parent); - tokenHolder.setOnTokenClickListener(onTokenClickListener); - holder = tokenHolder; - } - break; - case TotalBalanceHolder.VIEW_TYPE: { - holder = new TotalBalanceHolder(R.layout.item_total_balance, parent); - } - } - - return holder; - } - - @Override public void onBindViewHolder(BinderViewHolder holder, int position) { - holder.bind(items.get(position).value); - } - - @Override public int getItemViewType(int position) { - return items.get(position).viewType; - } - - @Override public int getItemCount() { - return items.size(); - } - - public void setTokens(Token[] tokens) { - items.beginBatchedUpdates(); - items.clear(); - items.add(total); - for (int i = 0; i < tokens.length; i++) { - items.add(new TokenSortedItem(tokens[i], 10 + i)); - } - items.endBatchedUpdates(); - } - - public void setTotal(BigDecimal totalInCurrency) { - total = new TotalBalanceSortedItem(totalInCurrency); - items.add(total); - } - - public void clear() { - items.clear(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TransactionsAdapter.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TransactionsAdapter.java index 310a3b651d3..6b3daa84560 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TransactionsAdapter.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TransactionsAdapter.java @@ -1,22 +1,35 @@ package com.asfoundation.wallet.ui.widget.adapter; import android.os.Bundle; -import android.support.v7.util.SortedList; -import android.support.v7.widget.RecyclerView; import android.view.ViewGroup; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SortedList; import com.asf.wallet.R; import com.asfoundation.wallet.entity.NetworkInfo; import com.asfoundation.wallet.entity.Wallet; +import com.asfoundation.wallet.promotions.PromotionNotification; +import com.asfoundation.wallet.referrals.CardNotification; import com.asfoundation.wallet.transactions.Transaction; +import com.asfoundation.wallet.ui.appcoins.applications.AppcoinsApplication; import com.asfoundation.wallet.ui.widget.OnTransactionClickListener; import com.asfoundation.wallet.ui.widget.entity.DateSortedItem; import com.asfoundation.wallet.ui.widget.entity.SortedItem; import com.asfoundation.wallet.ui.widget.entity.TimestampSortedItem; import com.asfoundation.wallet.ui.widget.entity.TransactionSortedItem; +import com.asfoundation.wallet.ui.widget.entity.TransactionsModel; +import com.asfoundation.wallet.ui.widget.holder.AppcoinsApplicationListViewHolder; +import com.asfoundation.wallet.ui.widget.holder.ApplicationClickAction; import com.asfoundation.wallet.ui.widget.holder.BinderViewHolder; +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction; +import com.asfoundation.wallet.ui.widget.holder.CardNotificationsListViewHolder; +import com.asfoundation.wallet.ui.widget.holder.PerkBonusViewHolder; import com.asfoundation.wallet.ui.widget.holder.TransactionDateHolder; import com.asfoundation.wallet.ui.widget.holder.TransactionHolder; +import com.asfoundation.wallet.util.CurrencyFormatUtils; import java.util.List; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; +import rx.functions.Action2; public class TransactionsAdapter extends RecyclerView.Adapter { @@ -51,26 +64,47 @@ public class TransactionsAdapter extends RecyclerView.Adapter } }); private final OnTransactionClickListener onTransactionClickListener; - + private final Action2 applicationClickListener; + private final Action2 referralNotificationClickListener; + private final CurrencyFormatUtils formatter; private Wallet wallet; private NetworkInfo network; - public TransactionsAdapter(OnTransactionClickListener onTransactionClickListener) { + public TransactionsAdapter(OnTransactionClickListener onTransactionClickListener, + Action2 applicationClickListener, + Action2 referralNotificationClickListener, + CurrencyFormatUtils formatter) { this.onTransactionClickListener = onTransactionClickListener; + this.applicationClickListener = applicationClickListener; + this.referralNotificationClickListener = referralNotificationClickListener; + this.formatter = formatter; } - @Override public BinderViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + @NotNull @Override + public BinderViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewType) { BinderViewHolder holder = null; switch (viewType) { - case TransactionHolder.VIEW_TYPE: { - TransactionHolder transactionHolder = - new TransactionHolder(R.layout.item_transaction, parent, onTransactionClickListener); - holder = transactionHolder; - } - break; - case TransactionDateHolder.VIEW_TYPE: { + case TransactionHolder.VIEW_TYPE: + holder = + new TransactionHolder(R.layout.item_transaction, parent, onTransactionClickListener, + formatter); + break; + case TransactionDateHolder.VIEW_TYPE: holder = new TransactionDateHolder(R.layout.item_transactions_date_head, parent); - } + break; + case AppcoinsApplicationListViewHolder.VIEW_TYPE: + holder = + new AppcoinsApplicationListViewHolder(R.layout.item_appcoins_application_list, parent, + applicationClickListener); + break; + case CardNotificationsListViewHolder.VIEW_TYPE: + holder = new CardNotificationsListViewHolder(R.layout.item_card_notifications_list, parent, + referralNotificationClickListener); + break; + case PerkBonusViewHolder.VIEW_TYPE: + holder = new PerkBonusViewHolder(R.layout.item_transaction_perk_bonus, parent, + onTransactionClickListener); + break; } return holder; } @@ -90,6 +124,26 @@ public TransactionsAdapter(OnTransactionClickListener onTransactionClickListener return items.size(); } + public int getTransactionsCount() { + int counter = 0; + for (int i = 0; i < items.size(); i++) { + if (items.get(i) instanceof TransactionSortedItem) { + counter++; + } + } + return counter; + } + + public int getNotificationsCount() { + int counter = 0; + for (int i = 0; i < items.size(); i++) { + if (items.get(i) instanceof CardNotificationSortedItem) { + counter += ((CardNotificationSortedItem) items.get(i)).value.size(); + } + } + return counter; + } + public void setDefaultWallet(Wallet wallet) { this.wallet = wallet; notifyDataSetChanged(); @@ -100,12 +154,30 @@ public void setDefaultNetwork(NetworkInfo network) { notifyDataSetChanged(); } - public void addTransactions(List transactions) { + public void addItems(TransactionsModel transactionsModel) { items.beginBatchedUpdates(); - for (Transaction transaction : transactions) { + + List notifications = transactionsModel.getNotifications(); + if (!notifications.isEmpty()) { + removeApps(); + items.add( + new CardNotificationSortedItem(notifications, CardNotificationsListViewHolder.VIEW_TYPE)); + } else { + transactionsModel.getApplications(); + if (!transactionsModel.getApplications() + .isEmpty()) { + items.add(new ApplicationSortedItem(transactionsModel.getApplications(), + AppcoinsApplicationListViewHolder.VIEW_TYPE)); + } + } + + for (Transaction transaction : transactionsModel.getTransactions()) { + int viewType = TransactionHolder.VIEW_TYPE; + if (transaction.getSubType() == Transaction.SubType.PERK_PROMOTION) { + viewType = PerkBonusViewHolder.VIEW_TYPE; + } TransactionSortedItem sortedItem = - new TransactionSortedItem(TransactionHolder.VIEW_TYPE, transaction, - TimestampSortedItem.DESC); + new TransactionSortedItem(viewType, transaction, TimestampSortedItem.DESC); items.add(sortedItem); items.add(DateSortedItem.round(transaction.getTimeStamp())); } @@ -115,4 +187,31 @@ public void addTransactions(List transactions) { public void clear() { items.clear(); } + + public void removeApps() { + for (int i = 0; i < items.size(); i++) { + if (items.get(i) instanceof ApplicationSortedItem) { + items.removeItemAt(i); + this.notifyItemChanged(i); + } + } + } + + public boolean removeItem(CardNotification cardNotification) { + for (int i = 0; i < items.size(); i++) { + if (items.get(i) instanceof CardNotificationSortedItem) { + CardNotificationSortedItem cardNotificationSortedItem = + (CardNotificationSortedItem) items.get(i); + List card = (List) cardNotificationSortedItem.value; + for (int j = 0; j < card.size(); j++) { + if (Objects.equals(card.get(j), cardNotification)) { + card.remove(j); + this.notifyItemChanged(i); + return card.size() == 0 || cardNotification instanceof PromotionNotification; + } + } + } + } + return false; + } } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TransactionsDetailsAdapter.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TransactionsDetailsAdapter.java index 1e16d0a7c6d..995765fab56 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TransactionsDetailsAdapter.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/TransactionsDetailsAdapter.java @@ -1,7 +1,7 @@ package com.asfoundation.wallet.ui.widget.adapter; import android.os.Bundle; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -40,7 +40,8 @@ public TransactionsDetailsAdapter(OnMoreClickListener onTransactionClickListener // the operation details so the user knows that there are more than one operation detail in the // transaction if (getItemCount() > 1) { - int itemWidth = (int) parent.getResources().getDimension(R.dimen.transaction_details_width); + int itemWidth = (int) parent.getResources() + .getDimension(R.dimen.transaction_details_width); ViewGroup.LayoutParams params = item.getLayoutParams(); params.width = itemWidth; item.setLayoutParams(params); @@ -49,10 +50,12 @@ public TransactionsDetailsAdapter(OnMoreClickListener onTransactionClickListener } @Override public void onBindViewHolder(BinderViewHolder holder, int position) { - Bundle addition = new Bundle(); - addition.putString(TransactionHolder.DEFAULT_ADDRESS_ADDITIONAL, wallet.address); - addition.putString(TransactionHolder.DEFAULT_SYMBOL_ADDITIONAL, network.symbol); - holder.bind(items.get(position), addition); + if (network != null) { + Bundle addition = new Bundle(); + addition.putString(TransactionHolder.DEFAULT_ADDRESS_ADDITIONAL, wallet.address); + addition.putString(TransactionHolder.DEFAULT_SYMBOL_ADDITIONAL, network.symbol); + holder.bind(items.get(position), addition); + } } @Override public int getItemViewType(int position) { @@ -64,9 +67,9 @@ public TransactionsDetailsAdapter(OnMoreClickListener onTransactionClickListener } /** - * Set the default wallet so we can check what is the current wallet in use + * Set the default wallet so we can check what is the current wallet in use * - * @param wallet The wallet object containing the current wallet information. + * @param wallet The wallet object containing the current wallet information. */ public void setDefaultWallet(Wallet wallet) { this.wallet = wallet; diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/WalletsAdapter.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/WalletsAdapter.java deleted file mode 100644 index bcb84c10f6a..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/adapter/WalletsAdapter.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.asfoundation.wallet.ui.widget.adapter; - -import android.os.Bundle; -import android.support.v7.widget.RecyclerView; -import android.view.ViewGroup; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.ui.widget.holder.BinderViewHolder; -import com.asfoundation.wallet.ui.widget.holder.WalletHolder; - -public class WalletsAdapter extends RecyclerView.Adapter { - - private final OnSetWalletDefaultListener onSetWalletDefaultListener; - private final OnWalletDeleteListener onWalletDeleteListener; - private final OnExportWalletListener onExportWalletListener; - - private Wallet[] wallets = new Wallet[0]; - - private Wallet defaultWallet = null; - - public WalletsAdapter(OnSetWalletDefaultListener onSetWalletDefaultListener, - OnWalletDeleteListener onWalletDeleteListener, - OnExportWalletListener onExportWalletListener) { - this.onSetWalletDefaultListener = onSetWalletDefaultListener; - this.onWalletDeleteListener = onWalletDeleteListener; - this.onExportWalletListener = onExportWalletListener; - } - - @Override public BinderViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - BinderViewHolder binderViewHolder = null; - switch (viewType) { - case WalletHolder.VIEW_TYPE: { - WalletHolder h = new WalletHolder(R.layout.item_wallet_manage, parent); - h.setOnSetWalletDefaultListener(onSetWalletDefaultListener); - h.setOnWalletDeleteListener(onWalletDeleteListener); - h.setOnExportWalletListener(onExportWalletListener); - binderViewHolder = h; - } - } - return binderViewHolder; - } - - @Override public void onBindViewHolder(BinderViewHolder holder, int position) { - switch (getItemViewType(position)) { - case WalletHolder.VIEW_TYPE: { - Wallet wallet = wallets[position]; - Bundle bundle = new Bundle(); - bundle.putBoolean(WalletHolder.IS_DEFAULT_ADDITION, - defaultWallet != null && defaultWallet.sameAddress(wallet.address)); - bundle.putBoolean(WalletHolder.IS_LAST_ITEM, getItemCount() == 1); - holder.bind(wallet, bundle); - } - break; - } - } - - @Override public int getItemViewType(int position) { - return WalletHolder.VIEW_TYPE; - } - - @Override public int getItemCount() { - return wallets.length; - } - - public void setWallets(Wallet[] wallets) { - this.wallets = wallets == null ? new Wallet[0] : wallets; - notifyDataSetChanged(); - } - - public Wallet getDefaultWallet() { - return defaultWallet; - } - - public void setDefaultWallet(Wallet wallet) { - this.defaultWallet = wallet; - notifyDataSetChanged(); - } - - public interface OnSetWalletDefaultListener { - void onSetDefault(Wallet wallet); - } - - public interface OnWalletDeleteListener { - void onDelete(Wallet delete); - } - - public interface OnExportWalletListener { - void onExport(Wallet wallet); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/DateSortedItem.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/DateSortedItem.java index 678e2b8496b..d83eb2decb7 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/DateSortedItem.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/DateSortedItem.java @@ -1,6 +1,5 @@ package com.asfoundation.wallet.ui.widget.entity; -import android.text.format.DateUtils; import com.asfoundation.wallet.ui.widget.holder.TransactionDateHolder; import java.util.Calendar; import java.util.Date; @@ -13,7 +12,7 @@ public DateSortedItem(Date value) { public static DateSortedItem round(long timeStampInSec) { Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - calendar.setTimeInMillis(timeStampInSec * DateUtils.SECOND_IN_MILLIS); + calendar.setTimeInMillis(timeStampInSec); calendar.set(Calendar.MILLISECOND, 999); calendar.set(Calendar.SECOND, 59); calendar.set(Calendar.MINUTE, 59); diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TimestampSortedItem.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TimestampSortedItem.java index bbcf896d7b5..cf28456ba59 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TimestampSortedItem.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TimestampSortedItem.java @@ -25,6 +25,6 @@ public TimestampSortedItem(int viewType, T value, int weight, int order) { return order * (getTimestamp().compareTo(otherTimestamp.getTimestamp()));/* ? 1 : getTimestamp() == otherTimestamp.getTimestamp() ? 0 : -1);*/ } - return Integer.MIN_VALUE; + return Integer.compare(weight, other.weight); } } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TokenSortedItem.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TokenSortedItem.java deleted file mode 100644 index 2a3acec530b..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TokenSortedItem.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.asfoundation.wallet.ui.widget.entity; - -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.ui.widget.holder.TokenHolder; - -public class TokenSortedItem extends SortedItem { - - public TokenSortedItem(Token value, int weight) { - super(TokenHolder.VIEW_TYPE, value, weight); - } - - @Override public int compare(SortedItem other) { - return weight - other.weight; - } - - @Override public boolean areContentsTheSame(SortedItem newItem) { - return false; - } - - @Override public boolean areItemsTheSame(SortedItem other) { - return other.viewType == TokenHolder.VIEW_TYPE - && ((TokenSortedItem) other).value.tokenInfo.address.equalsIgnoreCase( - value.tokenInfo.address); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TotalBalanceSortedItem.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TotalBalanceSortedItem.java deleted file mode 100644 index a223a36f7c1..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TotalBalanceSortedItem.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.asfoundation.wallet.ui.widget.entity; - -import com.asfoundation.wallet.ui.widget.holder.TotalBalanceHolder; -import java.math.BigDecimal; - -public class TotalBalanceSortedItem extends SortedItem { - - public TotalBalanceSortedItem(BigDecimal value) { - super(TotalBalanceHolder.VIEW_TYPE, value, 0); - } - - @Override public int compare(SortedItem other) { - return weight - other.weight; - } - - @Override public boolean areContentsTheSame(SortedItem newItem) { - return newItem.viewType == viewType || (((TotalBalanceSortedItem) newItem).value == null - && value == null) || (((TotalBalanceSortedItem) newItem).value != null - && value != null - && ((TotalBalanceSortedItem) newItem).value.compareTo(value) == 0); - } - - @Override public boolean areItemsTheSame(SortedItem other) { - return other.viewType == viewType; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TransactionSortedItem.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TransactionSortedItem.java index 8830d2c9498..43e6a9b415b 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TransactionSortedItem.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TransactionSortedItem.java @@ -1,7 +1,5 @@ package com.asfoundation.wallet.ui.widget.entity; -import android.text.format.DateUtils; -import com.asfoundation.wallet.entity.RawTransaction; import com.asfoundation.wallet.transactions.Transaction; import java.util.Calendar; import java.util.Date; @@ -16,24 +14,25 @@ public TransactionSortedItem(int viewType, Transaction value, int order) { @Override public boolean areContentsTheSame(SortedItem newItem) { if (viewType == newItem.viewType) { Transaction transaction = (Transaction) newItem.value; - return value.getTransactionId().equals(transaction.getTimeStamp()) && value.getTimeStamp() == transaction.getTimeStamp(); + return value.getTransactionId() + .equals(transaction.getTimeStamp()) && value.getTimeStamp() == transaction.getTimeStamp(); } return false; } @Override public boolean areItemsTheSame(SortedItem other) { - return viewType == other.viewType; + return viewType == other.viewType && ((TransactionSortedItem) other).value.getTransactionId() + .equalsIgnoreCase(value.getTransactionId()); } @Override public Date getTimestamp() { Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - calendar.setTimeInMillis(value.getTimeStamp() * DateUtils.SECOND_IN_MILLIS); + calendar.setTimeInMillis(value.getTimeStamp()); return calendar.getTime(); } @Override public int compare(SortedItem other) { - return - other.viewType == viewType && ((TransactionSortedItem) other).value.getTransactionId().equalsIgnoreCase( - value.getTransactionId()) ? 0 : super.compare(other); + return other.viewType == viewType && ((TransactionSortedItem) other).value.getTransactionId() + .equalsIgnoreCase(value.getTransactionId()) ? 0 : super.compare(other); } } diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TransactionsModel.kt b/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TransactionsModel.kt new file mode 100644 index 00000000000..d8884619893 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/entity/TransactionsModel.kt @@ -0,0 +1,11 @@ +package com.asfoundation.wallet.ui.widget.entity + +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.transactions.Transaction +import com.asfoundation.wallet.ui.appcoins.applications.AppcoinsApplication + +data class TransactionsModel( + val transactions: List, + val notifications: List, + val applications: List +) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/AppcoinsApplicationListViewHolder.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/AppcoinsApplicationListViewHolder.java new file mode 100644 index 00000000000..4323bc02525 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/AppcoinsApplicationListViewHolder.java @@ -0,0 +1,54 @@ +package com.asfoundation.wallet.ui.widget.holder; + +import android.os.Bundle; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.asf.wallet.R; +import com.asfoundation.wallet.ui.appcoins.AppcoinsApplicationAdapter; +import com.asfoundation.wallet.ui.appcoins.ItemDecorator; +import com.asfoundation.wallet.ui.appcoins.applications.AppcoinsApplication; +import java.util.ArrayList; +import java.util.List; +import rx.functions.Action2; + +public class AppcoinsApplicationListViewHolder extends BinderViewHolder> { + public static final int VIEW_TYPE = 1006; + private final AppcoinsApplicationAdapter adapter; + private final RecyclerView recyclerView; + private final View title; + private final View icon; + + public AppcoinsApplicationListViewHolder(int resId, ViewGroup parent, + Action2 applicationClickListener) { + super(resId, parent); + recyclerView = findViewById(R.id.recycler_view); + title = findViewById(R.id.title); + icon = findViewById(R.id.icon); + int space = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, + getContext().getResources() + .getDisplayMetrics()); + recyclerView.addItemDecoration(new ItemDecorator(space)); + recyclerView.setLayoutManager( + new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false)); + adapter = new AppcoinsApplicationAdapter(applicationClickListener, new ArrayList<>()); + recyclerView.setAdapter(adapter); + } + + @Override public void bind(@Nullable List data, @NonNull Bundle addition) { + if (data == null || data.isEmpty()) { + recyclerView.setVisibility(View.GONE); + title.setVisibility(View.GONE); + icon.setVisibility(View.GONE); + } else { + adapter.setApplications(data); + title.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.VISIBLE); + icon.setVisibility(View.VISIBLE); + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/AppcoinsApplicationViewHolder.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/AppcoinsApplicationViewHolder.java new file mode 100644 index 00000000000..47fe80e3145 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/AppcoinsApplicationViewHolder.java @@ -0,0 +1,180 @@ +package com.asfoundation.wallet.ui.widget.holder; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Shader; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.palette.graphics.Palette; +import androidx.recyclerview.widget.RecyclerView; +import com.asf.wallet.R; +import com.asfoundation.wallet.GlideApp; +import com.asfoundation.wallet.ui.appcoins.applications.AppcoinsApplication; +import com.asfoundation.wallet.widget.CardHeaderTransformation; +import com.bumptech.glide.load.MultiTransformation; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.CircleCrop; +import com.bumptech.glide.request.Request; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.SizeReadyCallback; +import com.bumptech.glide.request.target.Target; +import com.bumptech.glide.request.transition.Transition; +import rx.functions.Action2; + +public class AppcoinsApplicationViewHolder extends RecyclerView.ViewHolder { + + private final TextView appName; + private final Action2 applicationClickListener; + private final ImageView appIcon; + private final TextView appRating; + private final ImageView featuredGraphic; + private final ImageView shareIcon; + private final TextView shareTitle; + + public AppcoinsApplicationViewHolder(View itemView, + Action2 applicationClickListener) { + super(itemView); + appName = itemView.findViewById(R.id.app_name); + appIcon = itemView.findViewById(R.id.app_icon); + featuredGraphic = itemView.findViewById(R.id.featured_graphic); + appRating = itemView.findViewById(R.id.app_rating); + shareIcon = itemView.findViewById(R.id.share_icon); + shareTitle = itemView.findViewById(R.id.share_title); + this.applicationClickListener = applicationClickListener; + } + + public void bind(AppcoinsApplication appcoinsApplication) { + appName.setText(appcoinsApplication.getName()); + + Target marketBitmap = new Target() { + @Override public void onLoadStarted(@Nullable Drawable placeholder) { + appIcon.setImageDrawable(placeholder); + } + + @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { + ColorDrawable whiteBackground = new ColorDrawable(0xffff); + appIcon.setImageDrawable(whiteBackground); + featuredGraphic.setImageDrawable(whiteBackground); + } + + @Override public void onResourceReady(@NonNull Bitmap resource, + @Nullable Transition transition) { + appIcon.setImageBitmap(resource); + if (appcoinsApplication.getFeaturedGraphic() == null) { + loadDefaultFeaturedGraphic(resource); + } + } + + @Override public void onLoadCleared(@Nullable Drawable placeholder) { + } + + @Override public void getSize(@NonNull SizeReadyCallback cb) { + cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); + } + + @Override public void removeCallback(@NonNull SizeReadyCallback cb) { + } + + @Override public void onStart() { + } + + @Override public void onStop() { + } + + @Override public void onDestroy() { + } + + @Override public void setRequest(@Nullable Request request) { + } + + @Nullable @Override public Request getRequest() { + return null; + } + }; + appIcon.setTag(marketBitmap); + + GlideApp.with(itemView.getContext()) + .asBitmap() + .load(appcoinsApplication.getIcon()) + .apply(RequestOptions.bitmapTransform(new CircleCrop()) + .placeholder(android.R.drawable.progress_indeterminate_horizontal)) + .into(marketBitmap); + + int space = getSizeFromDp(itemView.getContext() + .getResources() + .getDisplayMetrics(), 8); + GlideApp.with(itemView.getContext()) + .load(appcoinsApplication.getFeaturedGraphic()) + .apply(RequestOptions.bitmapTransform( + new MultiTransformation<>(new CenterCrop(), new CardHeaderTransformation(space)))) + .into(featuredGraphic); + appRating.setText(String.valueOf(appcoinsApplication.getRating())); + setupClickListeners(appcoinsApplication); + } + + private void setupClickListeners(AppcoinsApplication appcoinsApplication) { + appName.setOnClickListener( + v -> applicationClickListener.call(appcoinsApplication, ApplicationClickAction.CLICK)); + appIcon.setOnClickListener( + v -> applicationClickListener.call(appcoinsApplication, ApplicationClickAction.CLICK)); + appRating.setOnClickListener( + v -> applicationClickListener.call(appcoinsApplication, ApplicationClickAction.CLICK)); + featuredGraphic.setOnClickListener( + v -> applicationClickListener.call(appcoinsApplication, ApplicationClickAction.CLICK)); + + shareIcon.setOnClickListener( + v -> applicationClickListener.call(appcoinsApplication, ApplicationClickAction.SHARE)); + shareTitle.setOnClickListener( + v -> applicationClickListener.call(appcoinsApplication, ApplicationClickAction.SHARE)); + } + + private void loadDefaultFeaturedGraphic(Bitmap bitmap) { + Palette.from(bitmap) + .generate(palette -> { + int dominantColor = palette.getDominantColor(0x36aeeb); + DisplayMetrics displayMetrics = itemView.getContext() + .getResources() + .getDisplayMetrics(); + int space = getSizeFromDp(displayMetrics, 8); + + Bitmap image = Bitmap.createBitmap(getSizeFromDp(displayMetrics, 260), + getSizeFromDp(displayMetrics, 16), Bitmap.Config.ARGB_8888); + image.eraseColor(dominantColor); + featuredGraphic.setImageBitmap( + addGradient(new CardHeaderTransformation(space).transform(image), + palette.getDominantColor(0x36aeeb), palette.getDominantColor(0x36aeeb) + 300)); + }); + } + + private int getSizeFromDp(DisplayMetrics displayMetrics, int value) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, displayMetrics); + } + + private Bitmap addGradient(Bitmap src, int color1, int color2) { + int w = src.getWidth(); + int h = src.getHeight(); + Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(result); + + canvas.drawBitmap(src, 0, 0, null); + + Paint paint = new Paint(); + LinearGradient shader = new LinearGradient(0, 0, w, 0, color1, color2, Shader.TileMode.CLAMP); + paint.setShader(shader); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.drawRect(0, 0, w, h, paint); + + return result; + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/ApplicationClickAction.kt b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/ApplicationClickAction.kt new file mode 100644 index 00000000000..1c72ccc7e31 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/ApplicationClickAction.kt @@ -0,0 +1,6 @@ +package com.asfoundation.wallet.ui.widget.holder + +enum class ApplicationClickAction { + CLICK, + SHARE +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/BinderViewHolder.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/BinderViewHolder.java index e29c0f48361..faffeaf6d32 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/BinderViewHolder.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/BinderViewHolder.java @@ -2,9 +2,9 @@ import android.content.Context; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/CardNotificationAction.kt b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/CardNotificationAction.kt new file mode 100644 index 00000000000..7bc453f1227 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/CardNotificationAction.kt @@ -0,0 +1,10 @@ +package com.asfoundation.wallet.ui.widget.holder + +enum class CardNotificationAction { + DISMISS, + DISCOVER, + UPDATE, + BACKUP, + DETAILS_URL, + NONE +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/CardNotificationViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/CardNotificationViewHolder.kt new file mode 100644 index 00000000000..33dece0e73c --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/CardNotificationViewHolder.kt @@ -0,0 +1,91 @@ +package com.asfoundation.wallet.ui.widget.holder + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R +import com.asfoundation.wallet.GlideApp +import com.asfoundation.wallet.interact.UpdateNotification +import com.asfoundation.wallet.promotions.PromotionNotification +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.referrals.ReferralNotification +import kotlinx.android.synthetic.main.item_card_notification.view.* +import kotlinx.android.synthetic.main.referral_notification_card.view.notification_dismiss_button +import rx.functions.Action2 + +class CardNotificationViewHolder( + itemView: View, + private val action: Action2 +) : RecyclerView.ViewHolder(itemView) { + + fun bind(cardNotification: CardNotification) { + setTitle(cardNotification) + setBody(cardNotification) + setImage(cardNotification) + setButtonActions(cardNotification) + } + + private fun setTitle(cardNotification: CardNotification) { + when (cardNotification) { + is ReferralNotification -> itemView.notification_title.text = + itemView.context.getString(cardNotification.title, + cardNotification.symbol + cardNotification.pendingAmount) + is PromotionNotification -> itemView.notification_title.text = cardNotification.noResTitle + else -> cardNotification.title?.let { + itemView.notification_title.text = itemView.context.getString(it) + } + } + } + + private fun setBody(cardNotification: CardNotification) { + if (cardNotification is PromotionNotification) { + itemView.notification_description.text = cardNotification.noResBody + } else { + cardNotification.body?.let { itemView.notification_description.setText(it) } + } + } + + private fun setImage(cardNotification: CardNotification) { + when (cardNotification) { + is UpdateNotification -> { + itemView.notification_animation.setAnimation(cardNotification.animation) + itemView.notification_image.visibility = View.INVISIBLE + itemView.notification_animation.visibility = View.VISIBLE + } + is PromotionNotification -> { + itemView.notification_image.visibility = View.VISIBLE + itemView.notification_animation.visibility = View.INVISIBLE + GlideApp.with(itemView.context) + .load(cardNotification.noResIcon) + .error(R.drawable.ic_promotions_default) + .circleCrop() + .into(itemView.notification_image) + } + else -> { + itemView.notification_image.visibility = View.VISIBLE + itemView.notification_animation.visibility = View.INVISIBLE + cardNotification.icon?.let { + itemView.notification_image.setImageResource(it) + itemView.notification_image.visibility = View.VISIBLE + } + } + } + } + + private fun setButtonActions(cardNotification: CardNotification) { + if (cardNotification is PromotionNotification) { + itemView.notification_dismiss_button.setOnClickListener { + action.call(cardNotification, CardNotificationAction.DISMISS) + } + itemView.setOnClickListener { action.call(cardNotification, cardNotification.positiveAction) } + itemView.notification_positive_button.visibility = View.GONE + } else { + cardNotification.positiveButtonText?.let { itemView.notification_positive_button.setText(it) } + itemView.notification_dismiss_button.setOnClickListener { + action.call(cardNotification, CardNotificationAction.DISMISS) + } + itemView.notification_positive_button.setOnClickListener { + action.call(cardNotification, cardNotification.positiveAction) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/CardNotificationsListViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/CardNotificationsListViewHolder.kt new file mode 100644 index 00000000000..1a4773b9f60 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/CardNotificationsListViewHolder.kt @@ -0,0 +1,42 @@ +package com.asfoundation.wallet.ui.widget.holder + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.asf.wallet.R +import com.asfoundation.wallet.referrals.CardNotification +import com.asfoundation.wallet.ui.appcoins.CardNotificationsItemDecorator +import com.asfoundation.wallet.ui.widget.adapter.CardNotificationsAdapter +import rx.functions.Action2 +import java.util.* + +class CardNotificationsListViewHolder(resId: Int, parent: ViewGroup, + action: Action2 +) : BinderViewHolder>(resId, parent) { + + private val adapter: CardNotificationsAdapter + private val recyclerView: RecyclerView = findViewById(R.id.recycler_view) + + init { + recyclerView.addItemDecoration(CardNotificationsItemDecorator()) + recyclerView.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + adapter = CardNotificationsAdapter(ArrayList(), action) + recyclerView.adapter = adapter + } + + override fun bind(data: List?, addition: Bundle) { + if (data == null || data.isEmpty()) { + recyclerView.visibility = View.GONE + } else { + adapter.notifications = data + adapter.notifyDataSetChanged() + recyclerView.visibility = View.VISIBLE + } + } + + companion object { + const val VIEW_TYPE = 1008 + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/PerkBonusViewHolder.kt b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/PerkBonusViewHolder.kt new file mode 100644 index 00000000000..7f18942452a --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/PerkBonusViewHolder.kt @@ -0,0 +1,51 @@ +package com.asfoundation.wallet.ui.widget.holder + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.GlideApp +import com.asfoundation.wallet.transactions.Transaction +import com.asfoundation.wallet.transactions.TransactionDetails +import com.asfoundation.wallet.ui.widget.OnTransactionClickListener +import kotlinx.android.synthetic.main.item_transaction_perk_bonus.view.* + +class PerkBonusViewHolder(resId: Int, + parent: ViewGroup, + private val onTransactionClickListener: OnTransactionClickListener) : + BinderViewHolder(resId, parent) { + + override fun bind(data: Transaction?, addition: Bundle) { + handleIcon(data?.details) + + if (!data?.title.isNullOrEmpty()) itemView.bonus_title.text = data?.title + else itemView.bonus_title.visibility = View.GONE + + itemView.bonus_description.text = data?.description + itemView.setOnClickListener { onClick(it, data) } + } + + private fun handleIcon(details: TransactionDetails?) { + var uri: String? = null + val icon: TransactionDetails.Icon? + if (details != null) { + icon = details.icon + when (icon?.type) { + TransactionDetails.Icon.Type.FILE -> uri = "file:" + icon.uri + TransactionDetails.Icon.Type.URL -> uri = icon.uri + } + } + GlideApp.with(context) + .load(uri) + .error(R.drawable.transactions_promotion_bonus) + .into(itemView.img) + } + + private fun onClick(view: View, transaction: Transaction?) { + transaction?.let { onTransactionClickListener.onTransactionClick(view, transaction) } + } + + companion object { + const val VIEW_TYPE = 1010 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TokenChangeHolder.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TokenChangeHolder.java deleted file mode 100644 index 8f03cf8a124..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TokenChangeHolder.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.asfoundation.wallet.ui.widget.holder; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Switch; -import android.widget.TextView; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.ui.widget.OnTokenClickListener; - -public class TokenChangeHolder extends BinderViewHolder implements View.OnClickListener { - - public static final int VIEW_TYPE = 1005; - private final TextView symbol; - private final Switch enableControl; - private final View deleteAction; - - private Token token; - private OnTokenClickListener onTokenClickListener; - private OnTokenClickListener onTokenDeleteClickListener; - - public TokenChangeHolder(int resId, ViewGroup parent) { - super(resId, parent); - - symbol = findViewById(R.id.symbol); - deleteAction = findViewById(R.id.delete_action); - enableControl = findViewById(R.id.is_enable); - deleteAction.setOnClickListener(this); - itemView.setOnClickListener(this); - } - - @Override public void bind(@Nullable Token data, @NonNull Bundle addition) { - if (data == null) { - return; - } - token = data; - if (TextUtils.isEmpty(token.tokenInfo.name)) { - symbol.setText(token.tokenInfo.symbol); - } else { - symbol.setText(token.tokenInfo.name + " (" + token.tokenInfo.symbol + ")"); - } - if (data.tokenInfo.isAddedManually) { - deleteAction.setVisibility(View.VISIBLE); - } else { - deleteAction.setVisibility(View.GONE); - } - enableControl.setChecked(data.tokenInfo.isEnabled); - } - - @Override public void onClick(View v) { - switch (v.getId()) { - case R.id.delete_action: { - if (onTokenDeleteClickListener != null) { - onTokenDeleteClickListener.onTokenClick(v, token); - } - } - break; - default: { - if (onTokenClickListener != null) { - enableControl.setChecked(!token.tokenInfo.isEnabled); - onTokenClickListener.onTokenClick(v, token); - } - } - } - } - - public void setOnTokenClickListener(OnTokenClickListener onTokenClickListener) { - this.onTokenClickListener = onTokenClickListener; - } - - public void setOnTokenDeleteClickListener(OnTokenClickListener onTokenClickListener) { - this.onTokenDeleteClickListener = onTokenClickListener; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TokenHolder.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TokenHolder.java deleted file mode 100644 index 3188776eb99..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TokenHolder.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.asfoundation.wallet.ui.widget.holder; - -import android.graphics.Color; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.content.ContextCompat; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.ForegroundColorSpan; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.TokenTicker; -import com.asfoundation.wallet.ui.widget.OnTokenClickListener; -import com.squareup.picasso.NetworkPolicy; -import com.squareup.picasso.Picasso; -import java.math.BigDecimal; -import java.math.RoundingMode; - -public class TokenHolder extends BinderViewHolder implements View.OnClickListener { - - public static final int VIEW_TYPE = 1005; - private static final String EMPTY_BALANCE = "\u2014\u2014"; - - private final TextView symbol; - private final TextView balanceEth; - private final TextView balanceCurrency; - private final ImageView icon; - - private Token token; - private OnTokenClickListener onTokenClickListener; - - public TokenHolder(int resId, ViewGroup parent) { - super(resId, parent); - - icon = findViewById(R.id.icon); - symbol = findViewById(R.id.symbol); - balanceEth = findViewById(R.id.balance_eth); - balanceCurrency = findViewById(R.id.balance_currency); - itemView.setOnClickListener(this); - } - - @Override public void bind(@Nullable Token data, @NonNull Bundle addition) { - this.token = data; - try { - // We handled NPE. Exception handling is expensive, but not impotent here - symbol.setText(TextUtils.isEmpty(token.tokenInfo.name) ? token.tokenInfo.symbol - : getString(R.string.token_name, token.tokenInfo.name, token.tokenInfo.symbol)); - - BigDecimal decimalDivisor = new BigDecimal(Math.pow(10, token.tokenInfo.decimals)); - BigDecimal ethBalance = - token.tokenInfo.decimals > 0 ? token.balance.divide(decimalDivisor) : token.balance; - ethBalance = ethBalance.setScale(4, RoundingMode.HALF_UP) - .stripTrailingZeros(); - String value = ethBalance.compareTo(BigDecimal.ZERO) == 0 ? "0" : ethBalance.toPlainString(); - this.balanceEth.setText(value); - TokenTicker ticker = token.ticker; - if (ticker == null) { - this.balanceCurrency.setText(EMPTY_BALANCE); - fillIcon(null, R.mipmap.token_logo); - } else { - fillCurrency(ethBalance, ticker); - fillIcon(ticker.image, R.mipmap.token_logo); - } - } catch (Exception ex) { - fillEmpty(); - } - } - - private void fillIcon(String imageUrl, int defaultResId) { - if (TextUtils.isEmpty(imageUrl)) { - icon.setImageResource(defaultResId); - } else { - Picasso.with(getContext()) - .load(imageUrl) - .networkPolicy(NetworkPolicy.OFFLINE) - .fit() - .centerInside() - .placeholder(defaultResId) - .error(defaultResId) - .into(icon); - } - } - - private void fillCurrency(BigDecimal ethBalance, TokenTicker ticker) { - String converted = ethBalance.compareTo(BigDecimal.ZERO) == 0 ? EMPTY_BALANCE - : ethBalance.multiply(new BigDecimal(ticker.price)) - .setScale(2, RoundingMode.HALF_UP) - .stripTrailingZeros() - .toPlainString(); - String formattedPercents = ""; - int color = Color.RED; - try { - double percentage = Double.valueOf(ticker.percentChange24h); - color = ContextCompat.getColor(getContext(), percentage < 0 ? R.color.red : R.color.green); - formattedPercents = "(" + (percentage < 0 ? "" : "+") + ticker.percentChange24h + "%)"; - } catch (Exception ex) { /* Quietly */ } - String lbl = - getString(R.string.token_balance, ethBalance.compareTo(BigDecimal.ZERO) == 0 ? "" : "$", - converted, formattedPercents); - Spannable spannable = new SpannableString(lbl); - spannable.setSpan(new ForegroundColorSpan(color), converted.length() + 1, lbl.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - this.balanceCurrency.setText(spannable); - } - - private void fillEmpty() { - balanceEth.setText(R.string.NA); - balanceCurrency.setText(EMPTY_BALANCE); - } - - @Override public void onClick(View v) { - if (onTokenClickListener != null) { - onTokenClickListener.onTokenClick(v, token); - } - } - - public void setOnTokenClickListener(OnTokenClickListener onTokenClickListener) { - this.onTokenClickListener = onTokenClickListener; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TotalBalanceHolder.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TotalBalanceHolder.java deleted file mode 100644 index a4f0d28b7a4..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TotalBalanceHolder.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.asfoundation.wallet.ui.widget.holder; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.view.ViewGroup; -import android.widget.TextView; -import com.asf.wallet.R; -import java.math.BigDecimal; -import java.math.RoundingMode; - -public class TotalBalanceHolder extends BinderViewHolder { - - public static final int VIEW_TYPE = 1006; - - private final TextView title; - - public TotalBalanceHolder(int resId, ViewGroup parent) { - super(resId, parent); - title = findViewById(R.id.title); - } - - @Override public void bind(@Nullable BigDecimal data, @NonNull Bundle addition) { - title.setText(data == null ? "--" : "$" + data.setScale(2, RoundingMode.HALF_UP) - .stripTrailingZeros() - .toPlainString()); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TransactionDateHolder.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TransactionDateHolder.java index aba18d24519..7503f33aa12 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TransactionDateHolder.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TransactionDateHolder.java @@ -1,8 +1,8 @@ package com.asfoundation.wallet.ui.widget.holder; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.text.format.DateFormat; import android.view.ViewGroup; import android.widget.TextView; diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TransactionDetailsHolder.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TransactionDetailsHolder.java index 58d3453ae05..c216cc076f1 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TransactionDetailsHolder.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TransactionDetailsHolder.java @@ -1,13 +1,12 @@ package com.asfoundation.wallet.ui.widget.holder; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.StringRes; -import android.text.TextUtils; import android.view.View; import android.widget.Button; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import com.asf.wallet.R; import com.asfoundation.wallet.transactions.Operation; import com.asfoundation.wallet.ui.widget.OnMoreClickListener; @@ -23,7 +22,7 @@ public class TransactionDetailsHolder extends BinderViewHolder public static final int VIEW_TYPE = 1007; /** Tag used to obtain wallet address in used */ - public static final String DEFAULT_ADDRESS_ADDITIONAL = "default_address"; + private static final String DEFAULT_ADDRESS_ADDITIONAL = "default_address"; /** The transaction operation item view */ private final View itemView; /** The operation transaction id */ @@ -61,7 +60,6 @@ public TransactionDetailsHolder(View view, OnMoreClickListener listener) { String currency = addition.getString(DEFAULT_SYMBOL_ADDITIONAL); - String peer = operation.getFrom(); int peerLabel = R.string.label_from; // Check if the from matches the current wallet address, ifo so then we change a label to "To" @@ -73,7 +71,8 @@ public TransactionDetailsHolder(View view, OnMoreClickListener listener) { more.setOnClickListener(this); - fill(operation.getTransactionId(), peerLabel, peer, operation.getFee() + " " + currency.toUpperCase()); + fill(operation.getTransactionId(), peerLabel, peer, + operation.getFee() + " " + currency.toUpperCase()); } private void fill(String transactionId, @StringRes int peerLabel, String peerAddress, diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TransactionHolder.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TransactionHolder.java index dc149ce4d49..846a6129109 100644 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TransactionHolder.java +++ b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/TransactionHolder.java @@ -1,24 +1,30 @@ package com.asfoundation.wallet.ui.widget.holder; +import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import com.asf.wallet.R; +import com.asfoundation.wallet.C; +import com.asfoundation.wallet.GlideApp; import com.asfoundation.wallet.transactions.Transaction; import com.asfoundation.wallet.transactions.TransactionDetails; import com.asfoundation.wallet.ui.widget.OnTransactionClickListener; -import com.asfoundation.wallet.widget.CircleTransformation; -import com.squareup.picasso.Picasso; +import com.asfoundation.wallet.util.CurrencyFormatUtils; +import com.asfoundation.wallet.util.WalletCurrency; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.load.resource.bitmap.CircleCrop; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.Target; import java.math.BigDecimal; -import java.math.RoundingMode; - -import static com.asfoundation.wallet.C.ETHER_DECIMALS; public class TransactionHolder extends BinderViewHolder implements View.OnClickListener { @@ -33,12 +39,13 @@ public class TransactionHolder extends BinderViewHolder private final TextView value; private final TextView currency; private final TextView status; - + private final OnTransactionClickListener onTransactionClickListener; + private final CurrencyFormatUtils formatter; private Transaction transaction; private String defaultAddress; - private OnTransactionClickListener onTransactionClickListener; - public TransactionHolder(int resId, ViewGroup parent, OnTransactionClickListener listener) { + public TransactionHolder(int resId, ViewGroup parent, OnTransactionClickListener listener, + CurrencyFormatUtils formatter) { super(resId, parent); srcImage = findViewById(R.id.img); @@ -49,6 +56,7 @@ public TransactionHolder(int resId, ViewGroup parent, OnTransactionClickListener currency = findViewById(R.id.currency); status = findViewById(R.id.status); onTransactionClickListener = listener; + this.formatter = formatter; itemView.setOnClickListener(this); } @@ -66,36 +74,107 @@ public TransactionHolder(int resId, ViewGroup parent, OnTransactionClickListener currency = transaction.getCurrency(); } - fill(transaction.getType(), transaction.getFrom(), transaction.getTo(), currency, - transaction.getValue(), ETHER_DECIMALS, transaction.getStatus(), transaction.getDetails()); + fill(transaction.getFrom(), transaction.getTo(), currency, transaction.getValue(), + transaction.getStatus(), transaction.getDetails()); } - private void fill(Transaction.TransactionType type, String from, String to, String currencySymbol, - String valueStr, long decimals, Transaction.TransactionStatus transactionStatus, - TransactionDetails details) { + private void fill(String from, String to, String currencySymbol, String valueStr, + Transaction.TransactionStatus transactionStatus, TransactionDetails details) { boolean isSent = from.toLowerCase() .equals(defaultAddress); - int transactionTypeIcon = R.drawable.ic_transaction_peer; + TransactionDetails.Icon icon; + String uri = null; + if (details != null) { + icon = details.getIcon(); + switch (icon.getType()) { + case FILE: + uri = "file:" + icon.getUri(); + break; + case URL: + uri = icon.getUri(); + break; + } + } - if (type == Transaction.TransactionType.ADS) { - transactionTypeIcon = R.drawable.ic_transaction_poa; - } else if (type == Transaction.TransactionType.IAB) { - transactionTypeIcon = R.drawable.ic_transaction_iab; + int transactionTypeIcon; + switch (transaction.getType()) { + case IAP: + case IAP_OFFCHAIN: + transactionTypeIcon = R.drawable.ic_transaction_iab; + setTypeIconVisibilityBasedOnDescription(details, uri); + break; + case ADS: + case ADS_OFFCHAIN: + transactionTypeIcon = R.drawable.ic_transaction_poa; + setTypeIconVisibilityBasedOnDescription(details, uri); + currencySymbol = WalletCurrency.CREDITS.getSymbol(); + break; + case BONUS: + typeIcon.setVisibility(View.GONE); + transactionTypeIcon = R.drawable.ic_transaction_peer; + currencySymbol = WalletCurrency.CREDITS.getSymbol(); + break; + case TOP_UP: + typeIcon.setVisibility(View.GONE); + transactionTypeIcon = R.drawable.transaction_type_top_up; + currencySymbol = WalletCurrency.CREDITS.getSymbol(); + break; + case TRANSFER_OFF_CHAIN: + typeIcon.setVisibility(View.GONE); + transactionTypeIcon = R.drawable.transaction_type_transfer_off_chain; + currencySymbol = WalletCurrency.CREDITS.getSymbol(); + break; + default: + transactionTypeIcon = R.drawable.ic_transaction_peer; + setTypeIconVisibilityBasedOnDescription(details, uri); } - if (details == null) { - srcImage.setImageResource(transactionTypeIcon); - typeIcon.setVisibility(View.GONE); + if (details != null) { + if (transaction.getType() + .equals(Transaction.TransactionType.BONUS)) { + address.setText(R.string.transaction_type_bonus); + } else if (transaction.getType() + .equals(Transaction.TransactionType.TOP_UP)) { + address.setText(R.string.topup_home_button); + } else if (transaction.getType() + .equals(Transaction.TransactionType.TRANSFER_OFF_CHAIN)) { + address.setText(R.string.transaction_type_p2p); + } else { + address.setText( + details.getSourceName() == null ? isSent ? to : from : getSourceText(transaction)); + } + description.setText(details.getDescription() == null ? "" : details.getDescription()); } else { - Picasso.with(getContext()) - .load("file:" + details.getIcon()) - .transform(new CircleTransformation()) - .into(srcImage); - ((ImageView) typeIcon.findViewById(R.id.icon)).setImageResource(transactionTypeIcon); - typeIcon.setVisibility(View.VISIBLE); + address.setText(isSent ? to : from); + description.setText(""); } + int finalTransactionTypeIcon = transactionTypeIcon; + + GlideApp.with(getContext()) + .load(uri) + .apply(RequestOptions.bitmapTransform(new CircleCrop()) + .placeholder(finalTransactionTypeIcon) + .error(transactionTypeIcon)) + .listener(new RequestListener() { + + @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, + Target target, boolean isFirstResource) { + typeIcon.setVisibility(View.GONE); + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, Target target, + DataSource dataSource, boolean isFirstResource) { + ((ImageView) typeIcon.findViewById(R.id.icon)).setImageResource( + finalTransactionTypeIcon); + return false; + } + }) + .into(srcImage); + int statusText = R.string.transaction_status_success; int statusColor = R.color.green; @@ -113,12 +192,10 @@ private void fill(Transaction.TransactionType type, String from, String to, Stri status.setText(statusText); status.setTextColor(ContextCompat.getColor(getContext(), statusColor)); - address.setText(details != null ? details.getSourceName() : isSent ? to : from); - description.setText(details != null ? details.getDescription() : ""); if (valueStr.equals("0")) { valueStr = "0 "; } else { - valueStr = (isSent ? "-" : "+") + getScaledValue(valueStr, decimals); + valueStr = (isSent ? "-" : "+") + getScaledValue(valueStr, C.ETHER_DECIMALS, currencySymbol); } currency.setText(currencySymbol); @@ -126,14 +203,30 @@ private void fill(Transaction.TransactionType type, String from, String to, Stri this.value.setText(valueStr); } - private String getScaledValue(String valueStr, long decimals) { - // Perform decimal conversion + private String getSourceText(Transaction transaction) { + if (transaction.getType() + .equals(Transaction.TransactionType.BONUS)) { + return getContext().getString(R.string.gamification_transaction_title, + transaction.getDetails() + .getSourceName()); + } + return transaction.getDetails() + .getSourceName(); + } + + private void setTypeIconVisibilityBasedOnDescription(TransactionDetails details, String uri) { + if (uri == null || details.getSourceName() == null) { + typeIcon.setVisibility(View.GONE); + } else { + typeIcon.setVisibility(View.VISIBLE); + } + } + + private String getScaledValue(String valueStr, long decimals, String currencySymbol) { + WalletCurrency walletCurrency = WalletCurrency.mapToWalletCurrency(currencySymbol); BigDecimal value = new BigDecimal(valueStr); value = value.divide(new BigDecimal(Math.pow(10, decimals))); - int scale = 4; //SIGNIFICANT_FIGURES - value.precision() + value.scale(); - return value.setScale(scale, RoundingMode.HALF_UP) - .stripTrailingZeros() - .toPlainString(); + return formatter.formatCurrency(value, walletCurrency); } @Override public void onClick(View view) { diff --git a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/WalletHolder.java b/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/WalletHolder.java deleted file mode 100644 index 2b9c290101d..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/ui/widget/holder/WalletHolder.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.asfoundation.wallet.ui.widget.holder; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.RadioButton; -import android.widget.TextView; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.ui.widget.adapter.WalletsAdapter; - -public class WalletHolder extends BinderViewHolder implements View.OnClickListener { - - public static final int VIEW_TYPE = 1001; - public final static String IS_DEFAULT_ADDITION = "is_default"; - public static final String IS_LAST_ITEM = "is_last"; - - private final RadioButton defaultAction; - private final ImageView deleteAction; - private final TextView address; - private final ImageView exportAction; - private WalletsAdapter.OnSetWalletDefaultListener onSetWalletDefaultListener; - private WalletsAdapter.OnWalletDeleteListener onWalletDeleteListener; - private WalletsAdapter.OnExportWalletListener onExportWalletListener; - private Wallet wallet; - - public WalletHolder(int resId, ViewGroup parent) { - super(resId, parent); - - defaultAction = findViewById(R.id.default_action); - deleteAction = findViewById(R.id.delete_action); - exportAction = findViewById(R.id.export_action); - address = findViewById(R.id.address); - - address.setOnClickListener(this); - defaultAction.setOnClickListener(this); - deleteAction.setOnClickListener(this); - exportAction.setOnClickListener(this); - } - - @Override public void bind(@Nullable Wallet data, @NonNull Bundle addition) { - wallet = null; - address.setText(null); - defaultAction.setEnabled(false); - if (data == null) { - return; - } - this.wallet = data; - address.setText(wallet.address); - defaultAction.setChecked(addition.getBoolean(IS_DEFAULT_ADDITION, false)); - defaultAction.setEnabled(true); - deleteAction.setVisibility( - addition.getBoolean(IS_DEFAULT_ADDITION, false) || addition.getBoolean(IS_LAST_ITEM, false) - ? View.GONE : View.VISIBLE); - } - - public void setOnSetWalletDefaultListener( - WalletsAdapter.OnSetWalletDefaultListener onSetWalletDefaultListener) { - this.onSetWalletDefaultListener = onSetWalletDefaultListener; - } - - public void setOnWalletDeleteListener( - WalletsAdapter.OnWalletDeleteListener onWalletDeleteListener) { - this.onWalletDeleteListener = onWalletDeleteListener; - } - - public void setOnExportWalletListener( - WalletsAdapter.OnExportWalletListener onExportWalletListener) { - this.onExportWalletListener = onExportWalletListener; - } - - @Override public void onClick(View view) { - switch (view.getId()) { - case R.id.address: - case R.id.default_action: { - if (onSetWalletDefaultListener != null) { - onSetWalletDefaultListener.onSetDefault(wallet); - } - } - break; - case R.id.delete_action: { - if (onWalletDeleteListener != null) { - onWalletDeleteListener.onDelete(wallet); - } - } - break; - case R.id.export_action: { - if (onExportWalletListener != null) { - onExportWalletListener.onExport(wallet); - } - } - break; - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/util/BalanceUtils.java b/app/src/main/java/com/asfoundation/wallet/util/BalanceUtils.java index bfdbd688e35..c5ee2d58762 100644 --- a/app/src/main/java/com/asfoundation/wallet/util/BalanceUtils.java +++ b/app/src/main/java/com/asfoundation/wallet/util/BalanceUtils.java @@ -1,40 +1,26 @@ package com.asfoundation.wallet.util; -import android.content.Context; -import android.content.res.Resources; import android.text.SpannableString; import android.text.Spanned; +import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.ForegroundColorSpan; import java.math.BigDecimal; import java.math.BigInteger; -import java.math.RoundingMode; import org.web3j.utils.Convert; public class BalanceUtils { - private static String WEI_IN_ETH = "1000000000000000000"; public static BigDecimal weiToEth(BigDecimal wei) { return Convert.fromWei(wei, Convert.Unit.ETHER); } - public static String ethToUsd(String priceUsd, String ethBalance) { - BigDecimal usd = new BigDecimal(ethBalance).multiply(new BigDecimal(priceUsd)); - usd = usd.setScale(2, RoundingMode.HALF_UP); - return usd.toString(); - } - - public static BigDecimal EthToWei(String eth) throws Exception { - return new BigDecimal(eth).multiply(new BigDecimal(WEI_IN_ETH)); - } - public static BigDecimal weiToGweiBI(BigInteger wei) { return Convert.fromWei(new BigDecimal(wei), Convert.Unit.GWEI); } - public static String weiToGwei(BigDecimal wei) { - return Convert.fromWei(wei, Convert.Unit.GWEI) - .toPlainString(); + public static BigDecimal weiToGwei(BigDecimal wei) { + return Convert.fromWei(wei, Convert.Unit.GWEI); } public static BigInteger gweiToWei(BigDecimal gwei) { @@ -52,33 +38,23 @@ public static BigInteger gweiToWei(BigDecimal gwei) { * @return amount in subunits */ public static BigDecimal baseToSubunit(BigDecimal baseAmount, int decimals) { - assert (decimals >= 0); + if ((decimals < 0)) throw new AssertionError(); return baseAmount.multiply(BigDecimal.valueOf(10) .pow(decimals)); } - /** - * @param subunitAmount - amouunt in subunits - * @param decimals - decimal places used to convert subunits to base - * - * @return amount in base units - */ - public static BigDecimal subunitToBase(BigDecimal subunitAmount, int decimals) { - return subunitAmount.divide(BigDecimal.valueOf(10) - .pow(decimals)); - } - - static public SpannableString formatBalance(String value, String currency, int currencySize, int currencyColor) { String balance = value + " " + currency.toUpperCase(); SpannableString styledTitle = new SpannableString(balance); - int currencyIndex = balance.indexOf(currency.toUpperCase()); - - styledTitle.setSpan(new AbsoluteSizeSpan(currencySize), currencyIndex, balance.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - styledTitle.setSpan(new ForegroundColorSpan(currencyColor), currencyIndex, balance.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + if (!TextUtils.isEmpty(currency)) { + int currencyIndex = balance.indexOf(currency.toUpperCase()); + + styledTitle.setSpan(new AbsoluteSizeSpan(currencySize), currencyIndex, balance.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + styledTitle.setSpan(new ForegroundColorSpan(currencyColor), currencyIndex, balance.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } return styledTitle; } } diff --git a/app/src/main/java/com/asfoundation/wallet/util/CurrencyFormatUtils.kt b/app/src/main/java/com/asfoundation/wallet/util/CurrencyFormatUtils.kt new file mode 100644 index 00000000000..5e3c5017f63 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/util/CurrencyFormatUtils.kt @@ -0,0 +1,134 @@ +package com.asfoundation.wallet.util + +import com.asfoundation.wallet.ui.transact.TransferFragmentView +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.DecimalFormat +import java.text.NumberFormat + +class CurrencyFormatUtils { + + companion object { + fun create(): CurrencyFormatUtils = CurrencyFormatUtils() + const val FIAT_SCALE = 2 + const val APPC_SCALE = 2 + const val CREDITS_SCALE = 2 + const val ETH_SCALE = 4 + } + + fun formatCurrency(value: BigDecimal, currencyType: WalletCurrency): String { + return when (currencyType) { + WalletCurrency.FIAT -> formatCurrencyFiat(value.toDouble()) + WalletCurrency.APPCOINS -> formatCurrencyAppcoins(value.toDouble()) + WalletCurrency.CREDITS -> formatCurrencyCredits(value.toDouble()) + WalletCurrency.ETHEREUM -> formatCurrencyEth(value.toDouble()) + } + } + + fun formatCurrency(value: String, currencyType: WalletCurrency): String { + return when (currencyType) { + WalletCurrency.FIAT -> formatCurrencyFiat(value.toDouble()) + WalletCurrency.APPCOINS -> formatCurrencyAppcoins(value.toDouble()) + WalletCurrency.CREDITS -> formatCurrencyCredits(value.toDouble()) + WalletCurrency.ETHEREUM -> formatCurrencyEth(value.toDouble()) + } + } + + fun formatCurrency(amount: BigDecimal): String { + val value = amount.toDouble() + return formatCurrencyFiat(value) + } + + private fun formatCurrencyFiat(value: Double): String { + val fiatFormatter = NumberFormat.getNumberInstance() + .apply { + minimumFractionDigits = FIAT_SCALE + maximumFractionDigits = FIAT_SCALE + roundingMode = RoundingMode.FLOOR + } + return fiatFormatter.format(value) + } + + private fun formatCurrencyAppcoins(value: Double): String { + val appcFormatter = NumberFormat.getNumberInstance() + .apply { + minimumFractionDigits = APPC_SCALE + maximumFractionDigits = APPC_SCALE + roundingMode = RoundingMode.FLOOR + } + return appcFormatter.format(value) + } + + private fun formatCurrencyCredits(value: Double): String { + val creditsFormatter = NumberFormat.getNumberInstance() + .apply { + minimumFractionDigits = CREDITS_SCALE + maximumFractionDigits = CREDITS_SCALE + roundingMode = RoundingMode.FLOOR + } + return creditsFormatter.format(value) + } + + private fun formatCurrencyEth(value: Double): String { + val ethFormatter = NumberFormat.getNumberInstance() + .apply { + minimumFractionDigits = ETH_SCALE + maximumFractionDigits = ETH_SCALE + roundingMode = RoundingMode.FLOOR + } + return ethFormatter.format(value) + } + + fun formatTransferCurrency(value: BigDecimal, currencyType: WalletCurrency): String { + val scale: Int = when (currencyType) { + WalletCurrency.APPCOINS -> APPC_SCALE + WalletCurrency.CREDITS -> CREDITS_SCALE + WalletCurrency.ETHEREUM -> ETH_SCALE + else -> throw IllegalArgumentException() + } + val transferFormatter = DecimalFormat("#,##0.00") + .apply { + minimumFractionDigits = scale + maximumFractionDigits = 15 + isParseBigDecimal = true + roundingMode = RoundingMode.FLOOR + } + return transferFormatter.format(value) + } + + fun formatGamificationValues(value: BigDecimal): String { + val formatter = DecimalFormat("#,###.##") + return formatter.format(value) + } + + fun scaleFiat(value: BigDecimal): BigDecimal = value.setScale(FIAT_SCALE, BigDecimal.ROUND_FLOOR) +} + + +enum class WalletCurrency(val symbol: String) { + FIAT(""), + APPCOINS("APPC"), + CREDITS("APPC-C"), + ETHEREUM("ETH"); + + companion object { + @JvmStatic + fun mapToWalletCurrency(currencySymbol: String): WalletCurrency { + return when (currencySymbol) { + "APPC" -> APPCOINS + "ETH" -> ETHEREUM + "APPC-C" -> CREDITS + else -> throw IllegalArgumentException() + } + } + + @JvmStatic + fun mapToWalletCurrency(currency: TransferFragmentView.Currency): WalletCurrency { + return when (currency) { + TransferFragmentView.Currency.APPC -> APPCOINS + TransferFragmentView.Currency.APPC_C -> CREDITS + TransferFragmentView.Currency.ETH -> ETHEREUM + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/util/DeviceInfo.kt b/app/src/main/java/com/asfoundation/wallet/util/DeviceInfo.kt new file mode 100644 index 00000000000..4a9aa44e780 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/util/DeviceInfo.kt @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.util + +class DeviceInfo(private val deviceManufacturer: String, private val deviceModel: String) { + + val manufacturer get() = deviceManufacturer + + val model get() = deviceModel +} diff --git a/app/src/main/java/com/asfoundation/wallet/util/EIPTransactionParser.java b/app/src/main/java/com/asfoundation/wallet/util/EIPTransactionParser.java new file mode 100644 index 00000000000..c955db06cfe --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/util/EIPTransactionParser.java @@ -0,0 +1,172 @@ +package com.asfoundation.wallet.util; + +import com.appcoins.wallet.billing.repository.entity.TransactionData; +import com.asfoundation.wallet.entity.TransactionBuilder; +import com.asfoundation.wallet.interact.DefaultTokenProvider; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import io.reactivex.Single; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; +import org.kethereum.erc681.ERC681; +import org.spongycastle.util.encoders.Hex; + +public class EIPTransactionParser { + private final DefaultTokenProvider defaultTokenProvider; + + public EIPTransactionParser(DefaultTokenProvider defaultTokenProvider) { + this.defaultTokenProvider = defaultTokenProvider; + } + + public Single buildTransaction(ERC681 erc681) { + switch (getTransactionType(erc681)) { + case APPC: + return buildAppcTransaction(erc681); + case TOKEN: + return buildTokenTransaction(erc681); + case ETH: + default: + return buildEthTransaction(erc681); + } + } + + private Single buildEthTransaction(ERC681 payment) { + TransactionBuilder transactionBuilder = new TransactionBuilder("ETH"); + transactionBuilder.toAddress(payment.getAddress()); + transactionBuilder.amount(getEtherTransferAmount(payment)); + return Single.just(transactionBuilder); + } + + private Single buildTokenTransaction(ERC681 payment) { + return defaultTokenProvider.getDefaultToken() + .flatMap(tokenInfo -> { + if (tokenInfo.address.equalsIgnoreCase(payment.getAddress())) { + return Single.just(tokenInfo); + } else { + return Single.error(new UnknownTokenException()); + } + }) + .map(tokenInfo -> new TransactionBuilder(tokenInfo.symbol, tokenInfo.address, + payment.getChainId(), getReceiverAddress(payment), + getTokenTransferAmount(payment, tokenInfo.decimals), + tokenInfo.decimals).shouldSendToken(true)); + } + + private Single buildAppcTransaction(ERC681 payment) { + return defaultTokenProvider.getDefaultToken() + .flatMap(tokenInfo -> { + if (tokenInfo.address.equalsIgnoreCase(payment.getAddress())) { + return Single.just(tokenInfo); + } else { + return Single.error(new UnknownTokenException()); + } + }) + .map(tokenInfo -> new TransactionBuilder(tokenInfo.symbol, getIabContractAddress(payment), + payment.getChainId(), getReceiverAddress(payment), + getTokenTransferAmount(payment, tokenInfo.decimals), getSkuId(payment), + tokenInfo.decimals, getIabContract(payment), getType(payment), getOrigin(payment), + getDomain(payment), getPayload(payment), null, getOrderReference(payment), null, + null).shouldSendToken(true)); + } + + private String getOrigin(ERC681 payment) { + return retrieveData(payment).getOrigin(); + } + + private String getOrderReference(ERC681 payment) { + return retrieveData(payment).getOrderReference(); + } + + private String getIabContract(ERC681 payment) { + return payment.getFunctionParams() + .get("iabContractAddress"); + } + + private String getIabContractAddress(ERC681 payment) { + return payment.getAddress(); + } + + private TransactionType getTransactionType(ERC681 payment) { + if (payment.getFunction() != null && payment.getFunction() + .equalsIgnoreCase("buy")) { + return TransactionType.APPC; + } else if (payment.getFunction() != null && payment.getFunction() + .equalsIgnoreCase("transfer")) { + return TransactionType.TOKEN; + } else { + return TransactionType.ETH; + } + } + + private String getSkuId(ERC681 payment) { + return retrieveData(payment).getSkuId(); + } + + private String getType(ERC681 payment) { + return retrieveData(payment).getType(); + } + + private String getDomain(ERC681 payment) { + return retrieveData(payment).getDomain(); + } + + private String getPayload(ERC681 payment) { + return retrieveData(payment).getPayload(); + } + + private TransactionData retrieveData(ERC681 payment) { + String data = new String(Hex.decode(payment.getFunctionParams() + .get("data") + .substring(2) + .getBytes(StandardCharsets.UTF_8))); + try { + return new Gson().fromJson(data, TransactionData.class); + } catch (JsonSyntaxException e) { + return new TransactionData(data); + } + } + + private BigDecimal getEtherTransferAmount(ERC681 payment) { + return convertToMainMetric(new BigDecimal(payment.getValue()), 18); + } + + private BigDecimal getTokenTransferAmount(ERC681 payment, int decimals) { + if (payment.getFunctionParams() + .containsKey("uint256")) { + return convertToMainMetric(new BigDecimal(payment.getFunctionParams() + .get("uint256")), decimals); + } + return BigDecimal.ZERO; + } + + private BigDecimal convertToMainMetric(BigDecimal value, int decimals) { + try { + StringBuilder divider = new StringBuilder(18); + divider.append("1"); + for (int i = 0; i < decimals; i++) { + divider.append("0"); + } + return value.divide(new BigDecimal(divider.toString()), decimals, RoundingMode.DOWN); + } catch (NumberFormatException ex) { + return BigDecimal.ZERO; + } + } + + private String getReceiverAddress(ERC681 payment) { + String address; + if (payment.getFunction() + .equals("transfer") || payment.getFunction() + .equals("buy")) { + address = payment.getFunctionParams() + .get("address"); + } else { + address = payment.getAddress(); + } + return address; + } + + private enum TransactionType { + APPC, TOKEN, ETH + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/util/Error.kt b/app/src/main/java/com/asfoundation/wallet/util/Error.kt new file mode 100644 index 00000000000..2cf9c9fbcdb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/util/Error.kt @@ -0,0 +1,3 @@ +package com.asfoundation.wallet.util + +data class Error(val hasError: Boolean = false, val isNoNetwork: Boolean = false) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/util/ExtensionFunctionUtils.kt b/app/src/main/java/com/asfoundation/wallet/util/ExtensionFunctionUtils.kt new file mode 100644 index 00000000000..b03d0d68ec5 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/util/ExtensionFunctionUtils.kt @@ -0,0 +1,88 @@ +package com.asfoundation.wallet.util + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.Point +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.view.WindowManager +import com.google.zxing.BarcodeFormat +import com.google.zxing.MultiFormatWriter +import com.journeyapps.barcodescanner.BarcodeEncoder +import retrofit2.HttpException +import java.io.IOException +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.DecimalFormat + +/** + * + * Class file to create kotlin extension functions + * + */ + +fun BigDecimal.scaleToString(scale: Int): String { + val format = DecimalFormat("#.##") + return format.format(this.setScale(scale, RoundingMode.FLOOR)) +} + +fun Throwable?.isNoNetworkException(): Boolean { + return this != null && (this is IOException || this.cause != null && this.cause is IOException) +} + +fun Bitmap.mergeWith(centeredImage: Bitmap): Bitmap { + + val combined = Bitmap.createBitmap(width, height, config) + val canvas = Canvas(combined) + canvas.drawBitmap(this, Matrix(), null) + + val resizeLogo = + Bitmap.createScaledBitmap(centeredImage, canvas.width / 5, canvas.height / 5, true) + val centreX = (canvas.width - resizeLogo.width) / 2f + val centreY = (canvas.height - resizeLogo.height) / 2f + canvas.drawBitmap(resizeLogo, centreX, centreY, null) + return combined +} + +fun String.generateQrCode(windowManager: WindowManager, logo: Drawable): Bitmap { + val size = Point() + windowManager.defaultDisplay + .getSize(size) + val imageSize = (size.x * 0.9).toInt() + val bitMatrix = + MultiFormatWriter().encode(this, BarcodeFormat.QR_CODE, imageSize, imageSize, + null) + val barcodeEncoder = BarcodeEncoder() + val qrCode = barcodeEncoder.createBitmap(bitMatrix) + return qrCode.mergeWith(logo.toBitmap()) +} + +fun Drawable.toBitmap(): Bitmap { + if (this is BitmapDrawable) { + return this.bitmap + } + val bitmap = + Bitmap.createBitmap(this.intrinsicWidth, this.intrinsicHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + this.setBounds(0, 0, canvas.width, canvas.height) + this.draw(canvas) + return bitmap +} + +fun HttpException.getMessage(): String { + val reader = this.response() + ?.errorBody() + ?.charStream() + val message = reader?.readText() + reader?.close() + return if (message.isNullOrBlank()) message() else message +} + +inline fun Iterable.sumByBigDecimal(selector: (T) -> BigDecimal): BigDecimal { + var sum = BigDecimal.ZERO + for (element in this) { + sum += selector(element) + } + return sum +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/util/KS.java b/app/src/main/java/com/asfoundation/wallet/util/KS.java index 5d9bf7a7186..43ac51197e6 100644 --- a/app/src/main/java/com/asfoundation/wallet/util/KS.java +++ b/app/src/main/java/com/asfoundation/wallet/util/KS.java @@ -42,7 +42,7 @@ @TargetApi(23) public class KS { private static final String TAG = "KS"; - private static final String ANDROID_KEY_STORE = "AndroidKeyStore"; + public static final String ANDROID_KEY_STORE = "AndroidKeyStore"; private static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC; private static final String PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7; private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding"; @@ -123,7 +123,7 @@ private synchronized static byte[] getData(final Context context, String alias, keyStore.load(null); SecretKey secretKey = (SecretKey) keyStore.getKey(alias, null); if (secretKey == null) { - /* no such key, the key is just simply not there */ + /* no such key, the key is just simply not there */ boolean fileExists = new File(encryptedDataFilePath).exists(); if (!fileExists) { return null;/* file also not there, fine then */ diff --git a/app/src/main/java/com/asfoundation/wallet/util/KeyboardUtils.java b/app/src/main/java/com/asfoundation/wallet/util/KeyboardUtils.java index aa0ff2cc1b9..00ff397fb59 100644 --- a/app/src/main/java/com/asfoundation/wallet/util/KeyboardUtils.java +++ b/app/src/main/java/com/asfoundation/wallet/util/KeyboardUtils.java @@ -10,7 +10,7 @@ public static void showKeyboard(View view) { InputMethodManager inputMethodManager = (InputMethodManager) view.getContext() .getSystemService(Context.INPUT_METHOD_SERVICE); if (inputMethodManager != null) { - inputMethodManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + inputMethodManager.showSoftInput(view, InputMethodManager.SHOW_FORCED); } } diff --git a/app/src/main/java/com/asfoundation/wallet/util/LogInterceptor.java b/app/src/main/java/com/asfoundation/wallet/util/LogInterceptor.java index 39dbb0de9b7..b90d65c96b6 100644 --- a/app/src/main/java/com/asfoundation/wallet/util/LogInterceptor.java +++ b/app/src/main/java/com/asfoundation/wallet/util/LogInterceptor.java @@ -1,11 +1,12 @@ package com.asfoundation.wallet.util; -import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; import java.io.IOException; import java.net.URLDecoder; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import okhttp3.Headers; import okhttp3.HttpUrl; @@ -20,7 +21,7 @@ public class LogInterceptor implements Interceptor { private static final String TAG = "HTTP_TRACE"; - private static final Charset UTF8 = Charset.forName("UTF-8"); + private static final Charset UTF8 = StandardCharsets.UTF_8; private static String requestPath(HttpUrl url) { String path = url.encodedPath(); @@ -29,100 +30,109 @@ private static String requestPath(HttpUrl url) { } @Override public Response intercept(@NonNull Chain chain) throws IOException { - Request request = chain.request(); - RequestBody requestBody = request.body(); StringBuilder logBuilder = new StringBuilder(); - logBuilder.append( - "<---------------------------BEGIN REQUEST---------------------------------->"); - logBuilder.append("\n"); - logBuilder.append("Request encoded url: ") - .append(request.method()) - .append(" ") - .append(requestPath(request.url())); - logBuilder.append("\n"); - String decodeUrl = requestDecodedPath(request.url()); - if (!TextUtils.isEmpty(decodeUrl)) { - logBuilder.append("Request decoded url: ") + Request request = chain.request(); + try { + RequestBody requestBody = request.body(); + logBuilder.append( + "<---------------------------BEGIN REQUEST---------------------------------->"); + logBuilder.append("\n"); + logBuilder.append("Request encoded url: ") .append(request.method()) .append(" ") - .append(decodeUrl); - } + .append(requestPath(request.url())); + logBuilder.append("\n"); + String decodeUrl = requestDecodedPath(request.url()); + if (!TextUtils.isEmpty(decodeUrl)) { + logBuilder.append("Request decoded url: ") + .append(request.method()) + .append(" ") + .append(decodeUrl); + } - Headers headers = request.headers(); - logBuilder.append("\n=============== Headers ===============\n"); - for (int i = headers.size() - 1; i > -1; i--) { - logBuilder.append(headers.name(i)) - .append(" : ") - .append(headers.get(headers.name(i))) - .append("\n"); - } - logBuilder.append("\n=============== END Headers ===============\n"); + Headers headers = request.headers(); + logBuilder.append("\n=============== Headers ===============\n"); + for (int i = headers.size() - 1; i > -1; i--) { + logBuilder.append(headers.name(i)) + .append(" : ") + .append(headers.get(headers.name(i))) + .append("\n"); + } + logBuilder.append("\n=============== END Headers ===============\n"); + + if (requestBody != null) { + Buffer buffer = new Buffer(); + requestBody.writeTo(buffer); - if (requestBody != null) { - Buffer buffer = new Buffer(); - requestBody.writeTo(buffer); + MediaType contentType = requestBody.contentType(); + if (contentType != null) { + contentType.charset(UTF8); + } - MediaType contentType = requestBody.contentType(); - if (contentType != null) { - contentType.charset(UTF8); + logBuilder.append(buffer.readString(UTF8)); } + long startNs = System.nanoTime(); + Response response = chain.proceed(request); + long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); - logBuilder.append(buffer.readString(UTF8)); - } - long startNs = System.nanoTime(); - Response response = chain.proceed(request); - long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + ResponseBody responseBody = response.body(); + logBuilder.append("\n"); + logBuilder.append("Response timeout: ") + .append(tookMs) + .append("ms"); + logBuilder.append("\n"); + logBuilder.append("Response message: ") + .append(response.message()); + logBuilder.append("\n"); + logBuilder.append("Response code: ") + .append(response.code()); - ResponseBody responseBody = response.body(); - logBuilder.append("\n"); - logBuilder.append("Response timeout: ") - .append(tookMs) - .append("ms"); - logBuilder.append("\n"); - logBuilder.append("Response message: ") - .append(response.message()); - logBuilder.append("\n"); - logBuilder.append("Response code: ") - .append(response.code()); + if (responseBody != null) { + BufferedSource source = responseBody.source(); + source.request(Long.MAX_VALUE); // Buffer the entire body. + Buffer buffer = source.getBuffer(); - if (responseBody != null) { - BufferedSource source = responseBody.source(); - source.request(Long.MAX_VALUE); // Buffer the entire body. - Buffer buffer = source.buffer(); + Charset charset = null; + MediaType contentType = responseBody.contentType(); + if (contentType != null) { + charset = contentType.charset(UTF8); + } - Charset charset = null; - MediaType contentType = responseBody.contentType(); - if (contentType != null) { - charset = contentType.charset(UTF8); - } + if (charset == null) { + charset = UTF8; + } - if (charset == null) { - charset = UTF8; + if (responseBody.contentLength() != 0) { + logBuilder.append("\n"); + logBuilder.append("Response body: \n") + .append(buffer.clone() + .readString(charset)); + } } - - if (responseBody.contentLength() != 0) { - logBuilder.append("\n"); - logBuilder.append("Response body: \n") - .append(buffer.clone() - .readString(charset)); + headers = response.headers(); + logBuilder.append("\n=============== Headers ===============\n"); + for (int i = headers.size() - 1; i > -1; i--) { + logBuilder.append(headers.name(i)) + .append(" : ") + .append(headers.get(headers.name(i))) + .append("\n"); } - } - headers = response.headers(); - logBuilder.append("\n=============== Headers ===============\n"); - for (int i = headers.size() - 1; i > -1; i--) { - logBuilder.append(headers.name(i)) - .append(" : ") - .append(headers.get(headers.name(i))) - .append("\n"); - } - logBuilder.append("\n=============== END Headers ===============\n"); + logBuilder.append("\n=============== END Headers ===============\n"); - logBuilder.append("\n"); - logBuilder.append( - "<-----------------------------END REQUEST--------------------------------->"); - logBuilder.append("\n\n\n"); - Log.d(TAG, logBuilder.toString()); - return response; + logBuilder.append("\n"); + logBuilder.append( + "<-----------------------------END REQUEST--------------------------------->"); + logBuilder.append("\n\n\n"); + Log.d(TAG, logBuilder.toString()); + return response; + } catch (Exception exception) { + logBuilder.append("Failed request url: ") + .append(request.method()) + .append(" ") + .append(requestPath(request.url())); + Log.e(TAG, logBuilder.toString()); + throw exception; + } } private String requestDecodedPath(HttpUrl url) { @@ -131,7 +141,7 @@ private String requestDecodedPath(HttpUrl url) { String query = URLDecoder.decode(url.encodedQuery(), "UTF-8"); return url.scheme() + "://" + url.host() + (query != null ? (path + '?' + query) : path); } catch (Exception ex) { - /* Quality */ + /* Quality */ } return null; } diff --git a/app/src/main/java/com/asfoundation/wallet/util/NumberFormatterUtils.kt b/app/src/main/java/com/asfoundation/wallet/util/NumberFormatterUtils.kt new file mode 100644 index 00000000000..8e5c9ed38ec --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/util/NumberFormatterUtils.kt @@ -0,0 +1,70 @@ +package com.asfoundation.wallet.util + +import java.text.DecimalFormat +import java.util.* + +class NumberFormatterUtils { + + companion object { + fun create(): NumberFormatterUtils = NumberFormatterUtils() + val suffixes = TreeMap() + } + + init { + suffixes[1000f] = "k" + suffixes[1000000f] = "M" + } + + fun formatNumberWithSuffix(value: Float): String { + if (value < 10000) return formatDecimalPlaces(value) + + val fetchLowestValueSuffix = suffixes.floorEntry(value) + val divideBy = fetchLowestValueSuffix.key + val suffix = fetchLowestValueSuffix.value + + val truncatedValue = value / (divideBy / 10) + val hasDecimal = + truncatedValue < 100 && truncatedValue / 10.0f != (truncatedValue / 10) + return if (hasDecimal) { + formatDecimalPlaces(truncatedValue / 10.0f) + suffix + } else { + formatDecimalPlaces((truncatedValue / 10)) + suffix + } + } + + fun formatNumberWithSuffix(value: Float, decimalPlaces: Int): String { + if (decimalPlaces < 0) return value.toString() + if (value < 10000) return formatDecimalPlaces(value) + + val fetchLowestValueSuffix = suffixes.floorEntry(value) + val divideBy = fetchLowestValueSuffix.key + val suffix = fetchLowestValueSuffix.value + + val truncatedValue = value / (divideBy / 10) + var formatString = "#" + if (decimalPlaces > 0) { + formatString += "." + for (i in 0 until decimalPlaces) { + formatString += "#" + } + } + val decimalFormatter = DecimalFormat(formatString) + return decimalFormatter.format(truncatedValue) + suffix + } + + private fun formatDecimalPlaces(value: Float): String { + val splitValue = value.toString() + .split(".") + return if (splitValue[1] != "0") { + value.toString() + } else { + removeDecimalPlaces(value) + } + } + + private fun removeDecimalPlaces(value: Float): String { + val splitValue = value.toString() + .split(".") + return splitValue[0] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/util/OneStepTransactionParser.kt b/app/src/main/java/com/asfoundation/wallet/util/OneStepTransactionParser.kt new file mode 100644 index 00000000000..843775aff99 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/util/OneStepTransactionParser.kt @@ -0,0 +1,158 @@ +package com.asfoundation.wallet.util + +import com.appcoins.wallet.bdsbilling.Billing +import com.appcoins.wallet.bdsbilling.ProxyService +import com.appcoins.wallet.billing.repository.entity.Product +import com.appcoins.wallet.commons.Repository +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.entity.Token +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.interact.DefaultTokenProvider +import com.asfoundation.wallet.service.TokenRateService +import io.reactivex.Single +import io.reactivex.functions.Function5 +import io.reactivex.schedulers.Schedulers +import java.math.BigDecimal +import java.math.RoundingMode + + +class OneStepTransactionParser( + private val proxyService: ProxyService, + private val billing: Billing, + private val conversionService: TokenRateService, + private val cache: Repository, + private val defaultTokenProvider: DefaultTokenProvider) { + + fun buildTransaction(oneStepUri: OneStepUri, referrerUrl: String): Single { + return if (cache.getSync(oneStepUri.toString()) != null) { + Single.just(cache.getSync(oneStepUri.toString())) + } else { + getSkuDetails(oneStepUri) + .flatMap { skuDetailsResponse: SkuDetailsResponse -> + Single.zip(getToken(), getIabContract(), getWallet(oneStepUri), getTokenContract(), + getAmount(oneStepUri, skuDetailsResponse.product), + Function5 { token: Token, iabContract: String, walletAddress: String, + tokenContract: String, amount: BigDecimal -> + TransactionBuilder(token.tokenInfo.symbol, tokenContract, getChainId(oneStepUri), + walletAddress, amount, getSkuId(oneStepUri), token.tokenInfo.decimals, + iabContract, Parameters.PAYMENT_TYPE_INAPP_UNMANAGED, null, + getDomain(oneStepUri), getPayload(oneStepUri), getCallback(oneStepUri), + getOrderReference(oneStepUri), referrerUrl, + skuDetailsResponse.product?.title.orEmpty()).shouldSendToken(true) + }) + .map { + it.originalOneStepValue = oneStepUri.parameters[Parameters.VALUE] + var currency = oneStepUri.parameters[Parameters.CURRENCY] + if (currency == null) { + currency = "APPC" + } + it.originalOneStepCurrency = currency + it + } + .doOnSuccess { transactionBuilder -> + cache.saveSync(oneStepUri.toString(), transactionBuilder) + } + } + .subscribeOn(Schedulers.io()) + } + } + + private fun getOrderReference(uri: OneStepUri): String? { + return uri.parameters["order_reference"] + } + + private fun getAmount(uri: OneStepUri, product: Product?): Single { + return when { + product != null -> Single.just(BigDecimal(product.price.appcoinsAmount)) + uri.parameters[Parameters.VALUE] != null -> getTransactionValue(uri) + else -> Single.error(MissingAmountException()) + } + } + + private fun getToAddress(uri: OneStepUri): String? { + return uri.parameters[Parameters.TO] + } + + private fun getSkuId(uri: OneStepUri): String? { + return uri.parameters[Parameters.PRODUCT] + } + + private fun getDomain(uri: OneStepUri): String? { + return uri.parameters[Parameters.DOMAIN] + } + + private fun getPayload(uri: OneStepUri): String? { + return uri.parameters[Parameters.DATA] + } + + private fun getCurrency(uri: OneStepUri): String? { + return uri.parameters[Parameters.CURRENCY] + } + + private fun getChainId(uri: OneStepUri): Long { + return if (uri.host == BuildConfig.PAYMENT_HOST_ROPSTEN_NETWORK) + Parameters.NETWORK_ID_ROPSTEN else Parameters.NETWORK_ID_MAIN + } + + private fun getToken(): Single { + return defaultTokenProvider.defaultToken + .map { Token(it, BigDecimal.ZERO) } + } + + private fun getIabContract(): Single { + return proxyService.getIabAddress(BuildConfig.DEBUG) + } + + private fun getTokenContract(): Single { + return proxyService.getAppCoinsAddress(BuildConfig.DEBUG) + } + + private fun getWallet(uri: OneStepUri): Single { + val domain = getDomain(uri) + val toAddressWallet = getToAddress(uri) + if (domain == null && toAddressWallet == null) { + return Single.error(MissingWalletException()) + } + + return if (domain != null) { + billing.getWallet(domain) + .onErrorReturn { + toAddressWallet + } + } else { + Single.just(toAddressWallet) + } + } + + private fun getSkuDetails(uri: OneStepUri): Single { + val domain = getDomain(uri) + val skuId = getSkuId(uri) + return if (domain != null && skuId != null) { + billing.getProducts(domain, listOf(skuId)) + .map { products -> SkuDetailsResponse(products[0]) } + .onErrorReturn { SkuDetailsResponse(null) } + } else Single.just(SkuDetailsResponse(null)) + } + + private fun getTransactionValue(uri: OneStepUri): Single { + return if (getCurrency(uri) == null || getCurrency(uri).equals("APPC", true)) { + Single.just(BigDecimal(uri.parameters[Parameters.VALUE]).setScale(18)) + } else { + conversionService.getAppcRate(getCurrency(uri)!!.toUpperCase()) + .map { + BigDecimal(uri.parameters[Parameters.VALUE]) + .divide(it.amount, 18, RoundingMode.UP) + } + } + } + + private fun getCallback(uri: OneStepUri): String? { + return uri.parameters[Parameters.CALLBACK_URL] + } +} + +class MissingWalletException : RuntimeException() + +class MissingAmountException : RuntimeException() + +class SkuDetailsResponse(val product: Product?) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/util/OneStepUri.kt b/app/src/main/java/com/asfoundation/wallet/util/OneStepUri.kt new file mode 100644 index 00000000000..3347a0b9582 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/util/OneStepUri.kt @@ -0,0 +1,8 @@ +package com.asfoundation.wallet.util + +data class OneStepUri ( + var scheme: String, + var host: String, + var path: String, + var parameters: MutableMap +) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/util/OneStepUriParser.kt b/app/src/main/java/com/asfoundation/wallet/util/OneStepUriParser.kt new file mode 100644 index 00000000000..330dc63e7ae --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/util/OneStepUriParser.kt @@ -0,0 +1,41 @@ +package com.asfoundation.wallet.util + +import android.net.Uri +import com.asf.wallet.BuildConfig + +class Parameters { + companion object { + const val VALUE = "value" + const val TO = "to" + const val PRODUCT = "product" + const val DOMAIN = "domain" + const val DATA = "data" + const val CURRENCY = "currency" + const val CALLBACK_URL = "callback_url" + const val SCHEME = "https" + const val HOST = BuildConfig.PAYMENT_HOST + const val SECOND_HOST = BuildConfig.SECOND_PAYMENT_HOST + const val PATH = "/transaction" + const val PAYMENT_TYPE_INAPP_UNMANAGED = "INAPP_UNMANAGED" + const val NETWORK_ID_ROPSTEN = 3L + const val NETWORK_ID_MAIN = 1L + } +} + +fun Uri.isOneStepURLString() = + scheme == Parameters.SCHEME && (host == Parameters.HOST || host == Parameters.SECOND_HOST) + && path.startsWith(Parameters.PATH) + +fun parseOneStep(uri: Uri): OneStepUri { + val scheme = uri.scheme + val host = uri.host + val path = uri.path + val parameters = mutableMapOf() + parameters.apply { + for (key in uri.queryParameterNames) { + this[key] = uri.getQueryParameter(key) + } + } + return OneStepUri(scheme, host, path, parameters) +} + diff --git a/app/src/main/java/com/asfoundation/wallet/util/PMMigrateHelper.java b/app/src/main/java/com/asfoundation/wallet/util/PMMigrateHelper.java deleted file mode 100644 index 48205e4f8d4..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/util/PMMigrateHelper.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.asfoundation.wallet.util; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Build; -import android.preference.PreferenceManager; -import com.asfoundation.wallet.entity.ServiceErrorException; -import com.wallet.pwd.trustapp.PasswordManager; -import java.util.Map; - -public class PMMigrateHelper { - public static void migrate(Context context) throws ServiceErrorException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - return; - } - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); - Map passwords = pref.getAll(); - for (String key : passwords.keySet()) { - if (key.contains("-pwd")) { - String address = key.replace("-pwd", ""); - try { - KS.put(context, address.toLowerCase(), PasswordManager.getPassword(address, context)); - } catch (ServiceErrorException ex) { - throw ex; - } catch (Exception ex) { - ex.printStackTrace(); - } - } - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/util/PasswordStoreFactory.java b/app/src/main/java/com/asfoundation/wallet/util/PasswordStoreFactory.java deleted file mode 100644 index 5b97c2ded8e..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/util/PasswordStoreFactory.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.asfoundation.wallet.util; - -import android.content.Context; -import android.os.Build; -import com.asfoundation.wallet.entity.ServiceErrorException; -import com.wallet.pwd.trustapp.PasswordManager; -import java.security.SecureRandom; - -public class PasswordStoreFactory { - public static void put(Context context, String address, String password) - throws ServiceErrorException { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - KS.put(context, address, password); - } else { - try { - PasswordManager.setPassword(address, password, context); - } catch (Exception e) { - throw new ServiceErrorException(ServiceErrorException.KEY_STORE_ERROR); - } - } - } - - public static byte[] get(Context context, String address) throws ServiceErrorException { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return KS.get(context, address); - } else { - try { - return PasswordManager.getPassword(address, context) - .getBytes(); - } catch (Exception e) { - throw new ServiceErrorException(ServiceErrorException.KEY_STORE_ERROR); - } - } - } - - public static void showAuthenticationScreen(Context context, int unlockScreenRequest) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - KS.showAuthenticationScreen(context, unlockScreenRequest); - } - } - - public static String generatePassword() { - byte bytes[] = new byte[256]; - SecureRandom random = new SecureRandom(); - random.nextBytes(bytes); - return String.valueOf(bytes); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/util/QRUri.java b/app/src/main/java/com/asfoundation/wallet/util/QRUri.java index 0394bb0237c..9a68497202c 100644 --- a/app/src/main/java/com/asfoundation/wallet/util/QRUri.java +++ b/app/src/main/java/com/asfoundation/wallet/util/QRUri.java @@ -1,13 +1,15 @@ package com.asfoundation.wallet.util; -import android.support.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import static com.asfoundation.wallet.ui.barcode.BarcodeCaptureActivity.ERROR_CODE; + /** * Created by marat on 10/11/17. * Parses out protocol, address and parameters from a URL originating in QR codes used by wallets & @@ -24,12 +26,10 @@ public class QRUri { private static final int ADDRESS_LENGTH = 42; - private final String protocol; private final String address; private final Map parameters; - private QRUri(String protocol, String address, Map parameters) { - this.protocol = protocol; + private QRUri(String address, Map parameters) { this.address = address; this.parameters = Collections.unmodifiableMap(parameters); } @@ -48,16 +48,15 @@ private static boolean isValidAddress(String address) { return isValidAddress(address) ? address : null; } - @Nullable public static QRUri parse(String url) { + public static QRUri parse(String url) { String[] parts = url.split(":"); - QRUri result = null; + QRUri result = new QRUri(ERROR_CODE, Collections.emptyMap()); if (parts.length == 1) { String address = extractAddress(parts[0]); if (!TextUtils.isEmpty(address)) { - result = new QRUri("", address.toLowerCase(), Collections.emptyMap()); + result = new QRUri(address.toLowerCase(), Collections.emptyMap()); } } else if (parts.length == 2) { - String protocol = parts[0]; String address = extractAddress(parts[1]); if (!TextUtils.isEmpty(address)) { Map params = new HashMap<>(); @@ -67,7 +66,7 @@ private static boolean isValidAddress(String address) { List paramParts = Arrays.asList(paramString.split("&")); params = parseParamsFromParamParts(paramParts); } - result = new QRUri(protocol, address, params); + result = new QRUri(address, params); } } return result; @@ -89,10 +88,6 @@ private static Map parseParamsFromParamParts(@Nullable List parse(String uri) { - if (ERC681ExtensionFunKt.isEthereumURLString(uri)) { - return Single.just(ERC681ParserKt.parseERC681(uri)) - .flatMap(erc681 -> { - switch (getTransactionType(erc681)) { - case APPC: - return buildAppcTransaction(erc681); - case TOKEN: - return buildTokenTransaction(erc681); - case ETH: - default: - return buildEthTransaction(erc681); - } - }); - } - return Single.error(new RuntimeException("is not an supported URI")); - } - - private Single buildEthTransaction(ERC681 payment) { - TransactionBuilder transactionBuilder = new TransactionBuilder("ETH"); - transactionBuilder.toAddress(payment.getAddress()); - transactionBuilder.amount(getEtherTransferAmount(payment)); - return Single.just(transactionBuilder); - } - - private Single buildTokenTransaction(ERC681 payment) { - return findDefaultWalletInteract.find() - .flatMap(wallet -> tokenRepository.fetchAll(wallet.address) - .flatMapIterable(Arrays::asList) - .filter(token -> token.tokenInfo.address.equalsIgnoreCase(payment.getAddress())) - .toList()) - .flatMap(tokens -> { - if (tokens.isEmpty()) { - return Single.error(new UnknownTokenException()); - } else { - return Single.just(tokens.get(0)); - } - }) - .map(token -> new TransactionBuilder(token.tokenInfo.symbol, token.tokenInfo.address, - payment.getChainId(), getReceiverAddress(payment), - getTokenTransferAmount(payment, token.tokenInfo.decimals), null, - token.tokenInfo.decimals).shouldSendToken(true)); - } - - private Single buildAppcTransaction(ERC681 payment) { - return findDefaultWalletInteract.find() - .flatMap(wallet -> tokenRepository.fetchAll(wallet.address) - .flatMapIterable(Arrays::asList) - .filter(token -> token.tokenInfo.address.equalsIgnoreCase(payment.getAddress())) - .toList()) - .flatMap(tokens -> { - if (tokens.isEmpty()) { - return Single.error(new UnknownTokenException()); - } else { - return Single.just(tokens.get(0)); - } - }) - .map(token -> new TransactionBuilder(token.tokenInfo.symbol, getIabContractAddress(payment), - payment.getChainId(), getReceiverAddress(payment), - getTokenTransferAmount(payment, token.tokenInfo.decimals), getSkuId(payment), - token.tokenInfo.decimals, getIabContract(payment)).shouldSendToken(true)); - } - - private String getIabContract(ERC681 payment) { - return payment.getFunctionParams() - .get("iabContractAddress"); - } - - private String getIabContractAddress(ERC681 payment) { - return payment.getAddress(); - } - - private TransactionType getTransactionType(ERC681 payment) { - if (payment.getFunction() != null && payment.getFunction() - .equalsIgnoreCase("buy")) { - return TransactionType.APPC; - } else if (payment.getFunction() != null && payment.getFunction() - .equalsIgnoreCase("transfer")) { - return TransactionType.TOKEN; - } else { - return TransactionType.ETH; - } - } - - private String getSkuId(ERC681 payment) throws UnsupportedEncodingException { - return new String(Hex.decode(payment.getFunctionParams() - .get("data") - .substring(2) - .getBytes("UTF-8"))); - } - - private BigDecimal getEtherTransferAmount(ERC681 payment) { - return convertToMainMetric(new BigDecimal(payment.getValue()), 18); - } - - private BigDecimal getTokenTransferAmount(ERC681 payment, int decimals) { - if (payment.getFunctionParams() - .containsKey("uint256")) { - return convertToMainMetric(new BigDecimal(payment.getFunctionParams() - .get("uint256")), decimals); - } - return BigDecimal.ZERO; - } - - private BigDecimal convertToMainMetric(BigDecimal value, int decimals) { - try { - StringBuilder divider = new StringBuilder(18); - divider.append("1"); - for (int i = 0; i < decimals; i++) { - divider.append("0"); - } - return value.divide(new BigDecimal(divider.toString())); - } catch (NumberFormatException ex) { - return BigDecimal.ZERO; - } - } - - private String getReceiverAddress(ERC681 payment) { - String address; - if (payment.getFunction() - .equals("transfer") || payment.getFunction() - .equals("buy")) { - address = payment.getFunctionParams() - .get("address"); - } else { - address = payment.getAddress(); - } - return address; - } - - private enum TransactionType { - APPC, TOKEN, ETH - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/util/TransferParser.kt b/app/src/main/java/com/asfoundation/wallet/util/TransferParser.kt new file mode 100644 index 00000000000..85b5e0e3f05 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/util/TransferParser.kt @@ -0,0 +1,23 @@ +package com.asfoundation.wallet.util + +import android.net.Uri +import com.asfoundation.wallet.entity.TransactionBuilder +import io.reactivex.Single +import org.kethereum.erc681.isEthereumURLString +import org.kethereum.erc681.parseERC681 + +class TransferParser(private val eipTransactionParser: EIPTransactionParser, + private val oneStepTransactionParser: OneStepTransactionParser) { + + fun parse(uri: String): Single { + if (uri.isEthereumURLString()) { + return Single.just(parseERC681(uri)) + .flatMap { erc681 -> eipTransactionParser.buildTransaction(erc681) } + } else if (Uri.parse(uri) + .isOneStepURLString()) { + return Single.just(parseOneStep(Uri.parse(uri))) + .flatMap { oneStepUri -> oneStepTransactionParser.buildTransaction(oneStepUri, uri) } + } + return Single.error(RuntimeException("is not an supported URI")) + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/util/UserAgentInterceptor.kt b/app/src/main/java/com/asfoundation/wallet/util/UserAgentInterceptor.kt new file mode 100644 index 00000000000..cc818a97388 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/util/UserAgentInterceptor.kt @@ -0,0 +1,69 @@ +package com.asfoundation.wallet.util + +import android.content.Context +import android.os.Build +import android.util.DisplayMetrics +import android.view.WindowManager +import com.asf.wallet.BuildConfig +import com.asfoundation.wallet.repository.PreferencesRepositoryType +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException +import java.util.* + +class UserAgentInterceptor(private val context: Context, + private val preferencesRepositoryType: PreferencesRepositoryType) : + Interceptor { + + private val userAgent: String + get() { + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val display = wm.defaultDisplay + val displayMetrics = DisplayMetrics() + display.getRealMetrics(displayMetrics) + val walletId = getOrCreateWalletId() + return ("AppCoins_Wallet/" + + BuildConfig.VERSION_NAME + + " (Linux; Android " + + Build.VERSION.RELEASE.replace(";".toRegex(), " ") + + "; " + + Build.VERSION.SDK_INT + + "; " + + Build.MODEL.replace(";".toRegex(), " ") + + " Build/" + + Build.PRODUCT.replace(";", " ") + + "; " + + System.getProperty("os.arch") + + "; " + + BuildConfig.APPLICATION_ID + + "; " + + BuildConfig.VERSION_CODE + + "; " + + walletId + + "; " + + displayMetrics.widthPixels + + "x" + + displayMetrics.heightPixels + + ")") + } + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + val requestWithUserAgent = originalRequest.newBuilder() + .header("User-Agent", userAgent) + .build() + return chain.proceed(requestWithUserAgent) + } + + private fun getOrCreateWalletId(): String { + var walletId = preferencesRepositoryType.getWalletId() + if (walletId == null) { + val randomId = UUID.randomUUID() + .toString() + preferencesRepositoryType.setWalletId(randomId) + walletId = randomId + } + return walletId + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/util/WalletUtils.java b/app/src/main/java/com/asfoundation/wallet/util/WalletUtils.java new file mode 100644 index 00000000000..0ba32da467e --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/util/WalletUtils.java @@ -0,0 +1,18 @@ +package com.asfoundation.wallet.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import org.web3j.crypto.CipherException; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.Wallet; +import org.web3j.crypto.WalletFile; + +public class WalletUtils { + public static Credentials loadCredentials(String password, String json) + throws IOException, CipherException { + ObjectMapper objectMapper = new ObjectMapper(); + + WalletFile walletFile = objectMapper.readValue(json, WalletFile.class); + return Credentials.create(Wallet.decrypt(password, walletFile)); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/AddTokenViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/AddTokenViewModel.java deleted file mode 100644 index 71bb2a49423..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/AddTokenViewModel.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; -import android.content.Context; -import com.asfoundation.wallet.interact.AddTokenInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.router.MyTokensRouter; - -public class AddTokenViewModel extends BaseViewModel { - - private final AddTokenInteract addTokenInteract; - private final FindDefaultWalletInteract findDefaultWalletInteract; - private final MyTokensRouter myTokensRouter; - - private final MutableLiveData result = new MutableLiveData<>(); - - AddTokenViewModel(AddTokenInteract addTokenInteract, - FindDefaultWalletInteract findDefaultWalletInteract, MyTokensRouter myTokensRouter) { - this.addTokenInteract = addTokenInteract; - this.findDefaultWalletInteract = findDefaultWalletInteract; - this.myTokensRouter = myTokensRouter; - } - - public void save(String address, String symbol, int decimals) { - addTokenInteract.add(address, symbol, decimals) - .subscribe(this::onSaved, this::onError); - } - - private void onSaved() { - progress.postValue(false); - result.postValue(true); - } - - public LiveData result() { - return result; - } - - public void showTokens(Context context) { - findDefaultWalletInteract.find() - .subscribe(w -> myTokensRouter.open(context, w), this::onError); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/AddTokenViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/AddTokenViewModelFactory.java deleted file mode 100644 index e84eed8ada2..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/AddTokenViewModelFactory.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.arch.lifecycle.ViewModel; -import android.arch.lifecycle.ViewModelProvider; -import android.support.annotation.NonNull; -import com.asfoundation.wallet.interact.AddTokenInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.router.MyTokensRouter; - -public class AddTokenViewModelFactory implements ViewModelProvider.Factory { - - private final AddTokenInteract addTokenInteract; - private final FindDefaultWalletInteract findDefaultWalletInteract; - private final MyTokensRouter myTokensRouter; - - public AddTokenViewModelFactory(AddTokenInteract addTokenInteract, - FindDefaultWalletInteract findDefaultWalletInteract, MyTokensRouter myTokensRouter) { - this.addTokenInteract = addTokenInteract; - this.findDefaultWalletInteract = findDefaultWalletInteract; - this.myTokensRouter = myTokensRouter; - } - - @NonNull @Override public T create(@NonNull Class modelClass) { - return (T) new AddTokenViewModel(addTokenInteract, findDefaultWalletInteract, myTokensRouter); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/AutoUpdateModel.kt b/app/src/main/java/com/asfoundation/wallet/viewmodel/AutoUpdateModel.kt new file mode 100644 index 00000000000..05978776bae --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/AutoUpdateModel.kt @@ -0,0 +1,11 @@ +package com.asfoundation.wallet.viewmodel + +import java.util.* + +data class AutoUpdateModel(val updateVersionCode: Int = -1, val updateMinSdk: Int = -1, + val blackList: List = Collections.emptyList()) { + + fun isValid(): Boolean { + return updateVersionCode != -1 + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/BaseNavigationActivity.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/BaseNavigationActivity.java index 84866de68df..3ebb90a7198 100644 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/BaseNavigationActivity.java +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/BaseNavigationActivity.java @@ -1,28 +1,19 @@ package com.asfoundation.wallet.viewmodel; -import android.support.annotation.MenuRes; -import android.support.annotation.NonNull; -import android.support.design.widget.BottomNavigationView; import android.view.MenuItem; +import androidx.annotation.NonNull; import com.asf.wallet.R; import com.asfoundation.wallet.ui.BaseActivity; +import com.google.android.material.bottomnavigation.BottomNavigationView; -public class BaseNavigationActivity extends BaseActivity +public abstract class BaseNavigationActivity extends BaseActivity implements BottomNavigationView.OnNavigationItemSelectedListener { - private BottomNavigationView navigation; - protected void initBottomNavigation() { - navigation = findViewById(R.id.bottom_navigation); + BottomNavigationView navigation = findViewById(R.id.bottom_navigation); navigation.setOnNavigationItemSelectedListener(this); } - protected void setBottomMenu(@MenuRes int menuRes) { - navigation.getMenu() - .clear(); - navigation.inflateMenu(menuRes); - } - @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { return false; } diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/BasePageviewFragment.kt b/app/src/main/java/com/asfoundation/wallet/viewmodel/BasePageviewFragment.kt new file mode 100644 index 00000000000..f2e9a6f8d92 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/BasePageviewFragment.kt @@ -0,0 +1,23 @@ +package com.asfoundation.wallet.viewmodel + +import android.os.Bundle +import com.asfoundation.wallet.App +import com.asfoundation.wallet.billing.analytics.PageViewAnalytics +import dagger.android.support.DaggerFragment + +abstract class BasePageViewFragment : DaggerFragment() { + + private lateinit var pageViewAnalytics: PageViewAnalytics + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + pageViewAnalytics = PageViewAnalytics((activity?.application as App).analyticsManager()) + } + + override fun onResume() { + super.onResume() + + pageViewAnalytics.sendPageViewEvent(javaClass.simpleName) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/BaseViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/BaseViewModel.java index bda03e4ac13..beb3b2806fd 100644 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/BaseViewModel.java +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/BaseViewModel.java @@ -1,10 +1,10 @@ package com.asfoundation.wallet.viewmodel; -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; -import android.arch.lifecycle.ViewModel; import android.text.TextUtils; import android.util.Log; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; import com.asfoundation.wallet.C; import com.asfoundation.wallet.entity.ErrorEnvelope; import io.reactivex.disposables.Disposable; @@ -34,7 +34,7 @@ public LiveData progress() { } protected void onError(Throwable throwable) { - Log.d("TAG", "Err", throwable); + Log.e("TAG", "Err", throwable); if (TextUtils.isEmpty(throwable.getMessage())) { error.postValue(new ErrorEnvelope(C.ErrorCode.UNKNOWN, null, throwable)); } else { diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/ConfirmationViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/ConfirmationViewModel.java deleted file mode 100644 index 5fbad21f834..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/ConfirmationViewModel.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.app.Activity; -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; -import com.asfoundation.wallet.entity.GasSettings; -import com.asfoundation.wallet.entity.PendingTransaction; -import com.asfoundation.wallet.entity.TransactionBuilder; -import com.asfoundation.wallet.interact.SendTransactionInteract; -import com.asfoundation.wallet.router.GasSettingsRouter; -import com.crashlytics.android.Crashlytics; - -public class ConfirmationViewModel extends BaseViewModel { - private final MutableLiveData transactionBuilder = new MutableLiveData<>(); - private final MutableLiveData transactionHash = new MutableLiveData<>(); - private final SendTransactionInteract sendTransactionInteract; - private final GasSettingsRouter gasSettingsRouter; - - ConfirmationViewModel(SendTransactionInteract sendTransactionInteract, - GasSettingsRouter gasSettingsRouter) { - this.sendTransactionInteract = sendTransactionInteract; - this.gasSettingsRouter = gasSettingsRouter; - } - - public void init(TransactionBuilder transactionBuilder) { - this.transactionBuilder.postValue(transactionBuilder); - } - - public LiveData transactionBuilder() { - return transactionBuilder; - } - - public LiveData transactionHash() { - return transactionHash; - } - - public void openGasSettings(Activity context) { - TransactionBuilder transactionBuilder = this.transactionBuilder.getValue(); - if (transactionBuilder != null) { - gasSettingsRouter.open(context, transactionBuilder.gasSettings()); - }/* else { - // TODO: Good idea return to SendActivity - }*/ - } - - private void onCreateTransaction(PendingTransaction pendingTransaction) { - transactionHash.postValue(pendingTransaction); - } - - public void send() { - progress.postValue(true); - disposable = sendTransactionInteract.send(transactionBuilder.getValue()) - .map(hash -> new PendingTransaction(hash, false)) - .subscribe(this::onCreateTransaction, this::onError); - } - - public void setGasSettings(GasSettings gasSettings) { - TransactionBuilder transactionBuilder = this.transactionBuilder.getValue(); - if (transactionBuilder != null) { - transactionBuilder.gasSettings(gasSettings); - this.transactionBuilder.postValue(transactionBuilder); // refresh view - }/* else { - // TODO: Good idea return to SendActivity - }*/ - } - - @Override protected void onError(Throwable throwable) { - super.onError(throwable); - Crashlytics.logException(throwable); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/ConfirmationViewModel.kt b/app/src/main/java/com/asfoundation/wallet/viewmodel/ConfirmationViewModel.kt new file mode 100644 index 00000000000..5e1e9cf5c03 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/ConfirmationViewModel.kt @@ -0,0 +1,95 @@ +package com.asfoundation.wallet.viewmodel + +import android.app.Activity +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.asfoundation.wallet.entity.GasSettings +import com.asfoundation.wallet.entity.PendingTransaction +import com.asfoundation.wallet.entity.TransactionBuilder +import com.asfoundation.wallet.interact.FetchGasSettingsInteract +import com.asfoundation.wallet.interact.SendTransactionInteract +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.router.GasSettingsRouter +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable + +class ConfirmationViewModel internal constructor( + private val sendTransactionInteract: SendTransactionInteract, + private val gasSettingsRouter: GasSettingsRouter, + private val gasSettingsInteract: FetchGasSettingsInteract, + private val logger: Logger) : BaseViewModel() { + + private val transactionBuilder = MutableLiveData() + private val transactionHash = MutableLiveData() + private var subscription: Disposable? = null + + companion object { + private val TAG = ConfirmationViewModel::class.java.simpleName + } + + fun init(transactionBuilder: TransactionBuilder) { + subscription = gasSettingsInteract.fetch(transactionBuilder.shouldSendToken()) + .doOnSuccess { gasSettings: GasSettings? -> + transactionBuilder.gasSettings(gasSettings) + this.transactionBuilder.postValue(transactionBuilder) + } + .subscribe({}, { throwable: Throwable -> onError(throwable) }) + } + + override fun onCleared() { + subscription?.let { + if (!it.isDisposed) { + it.dispose() + } + } + super.onCleared() + } + + override fun onError(throwable: Throwable) { + super.onError(throwable) + logger.log(TAG, throwable.message, throwable) + } + + fun transactionBuilder(): LiveData { + return transactionBuilder + } + + fun transactionHash(): LiveData { + return transactionHash + } + + fun openGasSettings(context: Activity?) { + val transactionBuilder = transactionBuilder.value + transactionBuilder?.let { + gasSettingsRouter.open(context, it.gasSettings()) + } + } + + private fun onCreateTransaction(pendingTransaction: PendingTransaction) { + transactionHash.postValue(pendingTransaction) + } + + fun progressFinished() { + progress.postValue(false) + } + + fun send() { + progress.postValue(true) + disposable = sendTransactionInteract.send(transactionBuilder.value) + .map { hash: String? -> PendingTransaction(hash, false) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { pendingTransaction: PendingTransaction -> + onCreateTransaction(pendingTransaction) + }) { throwable: Throwable -> onError(throwable) } + } + + fun setGasSettings(gasSettings: GasSettings?) { + val transactionBuilder = transactionBuilder.value + transactionBuilder?.let { + it.gasSettings(gasSettings) + this.transactionBuilder.postValue(it) // refresh view + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/ConfirmationViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/ConfirmationViewModelFactory.java index 99d43e6487c..5e0e38447da 100644 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/ConfirmationViewModelFactory.java +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/ConfirmationViewModelFactory.java @@ -1,23 +1,31 @@ package com.asfoundation.wallet.viewmodel; -import android.arch.lifecycle.ViewModel; -import android.arch.lifecycle.ViewModelProvider; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; +import com.asfoundation.wallet.interact.FetchGasSettingsInteract; import com.asfoundation.wallet.interact.SendTransactionInteract; +import com.asfoundation.wallet.logging.Logger; import com.asfoundation.wallet.router.GasSettingsRouter; public class ConfirmationViewModelFactory implements ViewModelProvider.Factory { private final SendTransactionInteract sendTransactionInteract; - private GasSettingsRouter gasSettingsRouter; + private final GasSettingsRouter gasSettingsRouter; + private final FetchGasSettingsInteract gasSettingsInteract; + private final Logger logger; public ConfirmationViewModelFactory(SendTransactionInteract sendTransactionInteract, - GasSettingsRouter gasSettingsRouter) { + GasSettingsRouter gasSettingsRouter, FetchGasSettingsInteract gasSettingsInteract, + Logger logger) { this.sendTransactionInteract = sendTransactionInteract; this.gasSettingsRouter = gasSettingsRouter; + this.gasSettingsInteract = gasSettingsInteract; + this.logger = logger; } @NonNull @Override public T create(@NonNull Class modelClass) { - return (T) new ConfirmationViewModel(sendTransactionInteract, gasSettingsRouter); + return (T) new ConfirmationViewModel(sendTransactionInteract, gasSettingsRouter, + gasSettingsInteract, logger); } } diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/CreateAccountViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/CreateAccountViewModelFactory.java index f260e04f213..6665bcae7ea 100644 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/CreateAccountViewModelFactory.java +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/CreateAccountViewModelFactory.java @@ -1,8 +1,8 @@ package com.asfoundation.wallet.viewmodel; -import android.arch.lifecycle.ViewModel; -import android.arch.lifecycle.ViewModelProvider; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; public class CreateAccountViewModelFactory implements ViewModelProvider.Factory { diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/GasSettingsViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/GasSettingsViewModel.java index 8accf27ba33..5eae7e6789e 100644 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/GasSettingsViewModel.java +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/GasSettingsViewModel.java @@ -1,7 +1,7 @@ package com.asfoundation.wallet.viewmodel; -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import com.asfoundation.wallet.entity.NetworkInfo; import com.asfoundation.wallet.interact.FindDefaultNetworkInteract; import java.math.BigDecimal; @@ -17,14 +17,14 @@ public class GasSettingsViewModel extends BaseViewModel { private MutableLiveData gasLimit = new MutableLiveData<>(); private MutableLiveData defaultNetwork = new MutableLiveData<>(); - public GasSettingsViewModel(FindDefaultNetworkInteract findDefaultNetworkInteract) { + GasSettingsViewModel(FindDefaultNetworkInteract findDefaultNetworkInteract) { this.findDefaultNetworkInteract = findDefaultNetworkInteract; gasPrice.setValue(BigInteger.ZERO); gasLimit.setValue(BigInteger.ZERO); } public void prepare() { - findDefaultNetworkInteract.find() + disposable = findDefaultNetworkInteract.find() .subscribe(this::onDefaultNetwork, this::onError); } diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/GasSettingsViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/GasSettingsViewModelFactory.java index 8492a6523e8..5433b617221 100644 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/GasSettingsViewModelFactory.java +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/GasSettingsViewModelFactory.java @@ -1,8 +1,8 @@ package com.asfoundation.wallet.viewmodel; -import android.arch.lifecycle.ViewModel; -import android.arch.lifecycle.ViewModelProvider; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; import com.asfoundation.wallet.interact.FindDefaultNetworkInteract; public class GasSettingsViewModelFactory implements ViewModelProvider.Factory { diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/ImportWalletViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/ImportWalletViewModel.java deleted file mode 100644 index 2af59137fed..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/ImportWalletViewModel.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; -import com.asfoundation.wallet.C; -import com.asfoundation.wallet.entity.ErrorEnvelope; -import com.asfoundation.wallet.entity.ServiceErrorException; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.interact.ImportWalletInteract; -import com.asfoundation.wallet.ui.widget.OnImportKeystoreListener; -import com.asfoundation.wallet.ui.widget.OnImportPrivateKeyListener; - -public class ImportWalletViewModel extends BaseViewModel - implements OnImportKeystoreListener, OnImportPrivateKeyListener { - - private final ImportWalletInteract importWalletInteract; - private final MutableLiveData wallet = new MutableLiveData<>(); - - ImportWalletViewModel(ImportWalletInteract importWalletInteract) { - this.importWalletInteract = importWalletInteract; - } - - @Override public void onKeystore(String keystore, String password) { - progress.postValue(true); - importWalletInteract.importKeystore(keystore, password) - .subscribe(this::onWallet, this::onError); - } - - @Override public void onPrivateKey(String key) { - progress.postValue(true); - importWalletInteract.importPrivateKey(key) - .subscribe(this::onWallet, this::onError); - } - - public LiveData wallet() { - return wallet; - } - - private void onWallet(Wallet wallet) { - progress.postValue(false); - this.wallet.postValue(wallet); - } - - public void onError(Throwable throwable) { - if (throwable.getCause() instanceof ServiceErrorException) { - if (((ServiceErrorException) throwable.getCause()).code == C.ErrorCode.ALREADY_ADDED) { - error.postValue(new ErrorEnvelope(C.ErrorCode.ALREADY_ADDED, null)); - } - } else { - error.postValue(new ErrorEnvelope(C.ErrorCode.UNKNOWN, throwable.getMessage())); - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/ImportWalletViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/ImportWalletViewModelFactory.java deleted file mode 100644 index 25712be9e17..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/ImportWalletViewModelFactory.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.arch.lifecycle.ViewModel; -import android.arch.lifecycle.ViewModelProvider; -import android.support.annotation.NonNull; -import com.asfoundation.wallet.interact.ImportWalletInteract; - -public class ImportWalletViewModelFactory implements ViewModelProvider.Factory { - - private final ImportWalletInteract importWalletInteract; - - public ImportWalletViewModelFactory(ImportWalletInteract importWalletInteract) { - this.importWalletInteract = importWalletInteract; - } - - @NonNull @Override public T create(@NonNull Class modelClass) { - return (T) new ImportWalletViewModel(importWalletInteract); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/MyAddressViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/MyAddressViewModel.java new file mode 100644 index 00000000000..ab25622428f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/MyAddressViewModel.java @@ -0,0 +1,17 @@ +package com.asfoundation.wallet.viewmodel; + +import android.content.Context; +import com.asfoundation.wallet.router.TransactionsRouter; + +public class MyAddressViewModel extends BaseViewModel { + + private final TransactionsRouter transactionsRouter; + + public MyAddressViewModel(TransactionsRouter transactionsRouter) { + this.transactionsRouter = transactionsRouter; + } + + public void showTransactions(Context context) { + transactionsRouter.open(context, true); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/MyAddressViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/MyAddressViewModelFactory.java new file mode 100644 index 00000000000..6a4d1579163 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/MyAddressViewModelFactory.java @@ -0,0 +1,19 @@ +package com.asfoundation.wallet.viewmodel; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; +import com.asfoundation.wallet.router.TransactionsRouter; + +public class MyAddressViewModelFactory implements ViewModelProvider.Factory { + + private final TransactionsRouter transactionsRouter; + + public MyAddressViewModelFactory(TransactionsRouter transactionsRouter) { + this.transactionsRouter = transactionsRouter; + } + + @NonNull @Override public T create(@NonNull Class modelClass) { + return (T) new MyAddressViewModel(transactionsRouter); + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/SendViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/SendViewModel.java index 44db5a4280d..84dcfa972b5 100644 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/SendViewModel.java +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/SendViewModel.java @@ -1,10 +1,11 @@ package com.asfoundation.wallet.viewmodel; import android.app.Activity; -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; +import android.content.Context; import android.content.Intent; import android.net.Uri; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import com.asfoundation.wallet.entity.Address; import com.asfoundation.wallet.entity.GasSettings; import com.asfoundation.wallet.entity.TransactionBuilder; @@ -13,6 +14,7 @@ import com.asfoundation.wallet.interact.FindDefaultWalletInteract; import com.asfoundation.wallet.router.ConfirmationRouter; import com.asfoundation.wallet.router.Result; +import com.asfoundation.wallet.router.TransactionsRouter; import com.asfoundation.wallet.util.QRUri; import com.asfoundation.wallet.util.TransferParser; import com.google.android.gms.vision.barcode.Barcode; @@ -20,6 +22,8 @@ import java.math.BigDecimal; import org.web3j.utils.Numeric; +import static com.asfoundation.wallet.ui.barcode.BarcodeCaptureActivity.ERROR_CODE; + public class SendViewModel extends BaseViewModel { private final MutableLiveData symbol = new MutableLiveData<>(); private final MutableLiveData address = new MutableLiveData<>(); @@ -30,15 +34,17 @@ public class SendViewModel extends BaseViewModel { private final ConfirmationRouter confirmationRouter; private final TransferParser transferParser; private final CompositeDisposable disposables; + private final TransactionsRouter transactionsRouter; private TransactionBuilder transactionBuilder; SendViewModel(FindDefaultWalletInteract findDefaultWalletInteract, FetchGasSettingsInteract fetchGasSettingsInteract, ConfirmationRouter confirmationRouter, - TransferParser transferParser) { + TransferParser transferParser, TransactionsRouter transactionsRouter) { this.findDefaultWalletInteract = findDefaultWalletInteract; this.fetchGasSettingsInteract = fetchGasSettingsInteract; this.confirmationRouter = confirmationRouter; this.transferParser = transferParser; + this.transactionsRouter = transactionsRouter; disposables = new CompositeDisposable(); } @@ -113,7 +119,8 @@ public boolean setAmount(String amount) { public boolean extractFromQR(Barcode barcode) { QRUri qrUrl = QRUri.parse(barcode.displayValue); - if (qrUrl != null) { + if (!qrUrl.getAddress() + .equals(ERROR_CODE)) { transactionBuilder.toAddress(qrUrl.getAddress()); if (qrUrl.getParameter("data") != null) { transactionBuilder.data( @@ -141,4 +148,8 @@ private void onDefaultWallet(Wallet wallet) { public boolean onActivityResult(int requestCode, int resultCode, Intent data) { return confirmationRouter.onActivityResult(requestCode, resultCode, data); } + + public void showTransactions(Context context) { + transactionsRouter.open(context, true); + } } diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/SendViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/SendViewModelFactory.java index e435aaeea3d..f248b13cf42 100644 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/SendViewModelFactory.java +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/SendViewModelFactory.java @@ -1,10 +1,11 @@ package com.asfoundation.wallet.viewmodel; -import android.arch.lifecycle.ViewModel; -import android.arch.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; import com.asfoundation.wallet.interact.FetchGasSettingsInteract; import com.asfoundation.wallet.interact.FindDefaultWalletInteract; import com.asfoundation.wallet.router.ConfirmationRouter; +import com.asfoundation.wallet.router.TransactionsRouter; import com.asfoundation.wallet.util.TransferParser; import io.reactivex.annotations.NonNull; @@ -14,18 +15,20 @@ public class SendViewModelFactory implements ViewModelProvider.Factory { private final FetchGasSettingsInteract fetchGasSettingsInteract; private final ConfirmationRouter confirmationRouter; private final TransferParser transferParser; + private final TransactionsRouter transactionsRouter; public SendViewModelFactory(FindDefaultWalletInteract findDefaultWalletInteract, FetchGasSettingsInteract fetchGasSettingsInteract, ConfirmationRouter confirmationRouter, - TransferParser transferParser) { + TransferParser transferParser, TransactionsRouter transactionsRouter) { this.findDefaultWalletInteract = findDefaultWalletInteract; this.fetchGasSettingsInteract = fetchGasSettingsInteract; this.confirmationRouter = confirmationRouter; this.transferParser = transferParser; + this.transactionsRouter = transactionsRouter; } @NonNull @Override public T create(@NonNull Class modelClass) { return (T) new SendViewModel(findDefaultWalletInteract, fetchGasSettingsInteract, - confirmationRouter, transferParser); + confirmationRouter, transferParser, transactionsRouter); } } diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/SplashViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/SplashViewModel.java deleted file mode 100644 index d9deb4d5a9c..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/SplashViewModel.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; -import android.arch.lifecycle.ViewModel; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.interact.FetchWalletsInteract; - -public class SplashViewModel extends ViewModel { - private final FetchWalletsInteract fetchWalletsInteract; - private MutableLiveData wallets = new MutableLiveData<>(); - - SplashViewModel(FetchWalletsInteract fetchWalletsInteract) { - this.fetchWalletsInteract = fetchWalletsInteract; - - fetchWalletsInteract.fetch() - .subscribe(wallets::postValue, this::onError); - } - - private void onError(Throwable throwable) { - wallets.postValue(new Wallet[0]); - } - - public LiveData wallets() { - return wallets; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/SplashViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/SplashViewModelFactory.java deleted file mode 100644 index 9b8a526636a..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/SplashViewModelFactory.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.arch.lifecycle.ViewModel; -import android.arch.lifecycle.ViewModelProvider; -import android.support.annotation.NonNull; -import com.asfoundation.wallet.interact.FetchWalletsInteract; - -public class SplashViewModelFactory implements ViewModelProvider.Factory { - - private final FetchWalletsInteract fetchWalletsInteract; - - public SplashViewModelFactory(FetchWalletsInteract fetchWalletsInteract) { - this.fetchWalletsInteract = fetchWalletsInteract; - } - - @NonNull @Override public T create(@NonNull Class modelClass) { - return (T) new SplashViewModel(fetchWalletsInteract); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/TokenChangeCollectionViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/TokenChangeCollectionViewModel.java deleted file mode 100644 index be2596bf6ba..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/TokenChangeCollectionViewModel.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; -import com.asfoundation.wallet.entity.ErrorEnvelope; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.interact.ChangeTokenEnableInteract; -import com.asfoundation.wallet.interact.DeleteTokenInteract; -import com.asfoundation.wallet.interact.FetchAllTokenInfoInteract; - -import static com.asfoundation.wallet.C.ErrorCode.EMPTY_COLLECTION; - -public class TokenChangeCollectionViewModel extends BaseViewModel { - private final MutableLiveData wallet = new MutableLiveData<>(); - private final MutableLiveData tokens = new MutableLiveData<>(); - - private final FetchAllTokenInfoInteract fetchAllTokenInfoInteract; - private final ChangeTokenEnableInteract changeTokenEnableInteract; - private final DeleteTokenInteract tokenDeleteInteract; - - TokenChangeCollectionViewModel(FetchAllTokenInfoInteract fetchAllTokenInfoInteract, - ChangeTokenEnableInteract changeTokenEnableInteract, - DeleteTokenInteract tokenDeleteInteract) { - this.fetchAllTokenInfoInteract = fetchAllTokenInfoInteract; - this.changeTokenEnableInteract = changeTokenEnableInteract; - this.tokenDeleteInteract = tokenDeleteInteract; - } - - public void prepare() { - progress.postValue(true); - fetchTokens(); - } - - public MutableLiveData wallet() { - return wallet; - } - - public LiveData tokens() { - return tokens; - } - - public void fetchTokens() { - progress.postValue(true); - disposable = fetchAllTokenInfoInteract.fetch(wallet.getValue()) - .subscribe(this::onTokens, this::onError, this::onFetchTokensCompletable); - } - - private void onFetchTokensCompletable() { - progress.postValue(false); - Token[] tokens = tokens().getValue(); - if (tokens == null || tokens.length == 0) { - error.postValue(new ErrorEnvelope(EMPTY_COLLECTION, "tokens not found")); - } - } - - private void onTokens(Token[] tokens) { - this.tokens.setValue(tokens); - if (tokens != null && tokens.length > 0) { - progress.postValue(true); - } - } - - public void setEnabled(Token token) { - changeTokenEnableInteract.setEnable(wallet.getValue(), token) - .subscribe(() -> { - }, this::onError); - } - - public void deleteToken(Token token) { - tokenDeleteInteract.delete(wallet.getValue(), token) - .subscribe(this::fetchTokens, t -> { - - }); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/TokenChangeCollectionViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/TokenChangeCollectionViewModelFactory.java deleted file mode 100644 index 49fe9eeb08f..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/TokenChangeCollectionViewModelFactory.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.arch.lifecycle.ViewModel; -import android.arch.lifecycle.ViewModelProvider; -import android.support.annotation.NonNull; -import com.asfoundation.wallet.interact.ChangeTokenEnableInteract; -import com.asfoundation.wallet.interact.DeleteTokenInteract; -import com.asfoundation.wallet.interact.FetchAllTokenInfoInteract; - -public class TokenChangeCollectionViewModelFactory implements ViewModelProvider.Factory { - - private final FetchAllTokenInfoInteract fetchAllTokenInfoInteract; - private final DeleteTokenInteract deleteTokenInteract; - private final ChangeTokenEnableInteract changeTokenEnableInteract; - - public TokenChangeCollectionViewModelFactory(FetchAllTokenInfoInteract fetchAllTokenInfoInteract, - ChangeTokenEnableInteract changeTokenEnableInteract, - DeleteTokenInteract deleteTokenInteract) { - this.fetchAllTokenInfoInteract = fetchAllTokenInfoInteract; - this.deleteTokenInteract = deleteTokenInteract; - this.changeTokenEnableInteract = changeTokenEnableInteract; - } - - @NonNull @Override public T create(@NonNull Class modelClass) { - return (T) new TokenChangeCollectionViewModel(fetchAllTokenInfoInteract, - changeTokenEnableInteract, deleteTokenInteract); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/TokensViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/TokensViewModel.java deleted file mode 100644 index 76886476ef6..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/TokensViewModel.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; -import android.content.Context; -import android.text.TextUtils; -import com.asfoundation.wallet.entity.ErrorEnvelope; -import com.asfoundation.wallet.entity.Token; -import com.asfoundation.wallet.entity.TokenInfo; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.interact.FetchTokensInteract; -import com.asfoundation.wallet.router.AddTokenRouter; -import com.asfoundation.wallet.router.ChangeTokenCollectionRouter; -import com.asfoundation.wallet.router.SendRouter; -import com.asfoundation.wallet.router.TransactionsRouter; -import java.math.BigDecimal; - -import static com.asfoundation.wallet.C.ErrorCode.EMPTY_COLLECTION; - -public class TokensViewModel extends BaseViewModel { - private final MutableLiveData wallet = new MutableLiveData<>(); - private final MutableLiveData tokens = new MutableLiveData<>(); - private final MutableLiveData total = new MutableLiveData<>(); - - private final FetchTokensInteract fetchTokensInteract; - private final AddTokenRouter addTokenRouter; - private final SendRouter sendRouter; - private final TransactionsRouter transactionsRouter; - private final ChangeTokenCollectionRouter changeTokenCollectionRouter; - - TokensViewModel(FetchTokensInteract fetchTokensInteract, AddTokenRouter addTokenRouter, - SendRouter sendRouter, TransactionsRouter transactionsRouter, - ChangeTokenCollectionRouter changeTokenCollectionRouter) { - this.fetchTokensInteract = fetchTokensInteract; - this.addTokenRouter = addTokenRouter; - this.sendRouter = sendRouter; - this.transactionsRouter = transactionsRouter; - this.changeTokenCollectionRouter = changeTokenCollectionRouter; - } - - public MutableLiveData wallet() { - return wallet; - } - - public LiveData tokens() { - return tokens; - } - - public LiveData total() { - return total; - } - - public void fetchTokens() { - progress.postValue(true); - fetchTokensInteract.fetch(wallet.getValue()) - .subscribe(this::onTokens, this::onError, this::onFetchTokensCompletable); - } - - private void onFetchTokensCompletable() { - progress.postValue(false); - Token[] tokens = tokens().getValue(); - if (tokens == null || tokens.length == 0) { - error.postValue(new ErrorEnvelope(EMPTY_COLLECTION, "tokens not found")); - } - } - - private void onTokens(Token[] tokens) { - this.tokens.setValue(tokens); - if (tokens != null && tokens.length > 0) { - progress.postValue(true); - showTotalBalance(tokens); - } - } - - private void showTotalBalance(Token[] tokens) { - BigDecimal total = new BigDecimal("0"); - for (Token token : tokens) { - if (token.balance != null - && token.ticker != null - && !TextUtils.isEmpty(token.ticker.price) - && token.balance.compareTo(BigDecimal.ZERO) != 0) { - BigDecimal decimalDivisor = new BigDecimal(Math.pow(10, token.tokenInfo.decimals)); - BigDecimal ethBalance = - token.tokenInfo.decimals > 0 ? token.balance.divide(decimalDivisor) : token.balance; - total = total.add(ethBalance.multiply(new BigDecimal(token.ticker.price))); - } - } - total = total.setScale(2, BigDecimal.ROUND_HALF_UP) - .stripTrailingZeros(); - if (total.compareTo(BigDecimal.ZERO) == 0) { - total = null; - } - this.total.postValue(total); - } - - public void showAddToken(Context context) { - addTokenRouter.open(context); - } - - public void showSendToken(Context context, String address, String symbol, int decimals) { - sendRouter.open(context, new TokenInfo(address, "", symbol, decimals, true, false)); - } - - public void showTransactions(Context context) { - transactionsRouter.open(context, true); - } - - public void showEditTokens(Context context) { - changeTokenCollectionRouter.open(context, wallet.getValue()); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/TokensViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/TokensViewModelFactory.java deleted file mode 100644 index 251e83e2624..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/TokensViewModelFactory.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.arch.lifecycle.ViewModel; -import android.arch.lifecycle.ViewModelProvider; -import android.support.annotation.NonNull; -import com.asfoundation.wallet.interact.FetchTokensInteract; -import com.asfoundation.wallet.router.AddTokenRouter; -import com.asfoundation.wallet.router.ChangeTokenCollectionRouter; -import com.asfoundation.wallet.router.SendRouter; -import com.asfoundation.wallet.router.TransactionsRouter; - -public class TokensViewModelFactory implements ViewModelProvider.Factory { - - private final FetchTokensInteract fetchTokensInteract; - private final AddTokenRouter addTokenRouter; - private final SendRouter sendRouter; - private final TransactionsRouter transactionsRouter; - private final ChangeTokenCollectionRouter changeTokenCollectionRouter; - - public TokensViewModelFactory(FetchTokensInteract fetchTokensInteract, - AddTokenRouter addTokenRouter, SendRouter sendRouter, TransactionsRouter transactionsRouter, - ChangeTokenCollectionRouter changeTokenCollectionRouter) { - this.fetchTokensInteract = fetchTokensInteract; - this.addTokenRouter = addTokenRouter; - this.sendRouter = sendRouter; - this.transactionsRouter = transactionsRouter; - this.changeTokenCollectionRouter = changeTokenCollectionRouter; - } - - @NonNull @Override public T create(@NonNull Class modelClass) { - return (T) new TokensViewModel(fetchTokensInteract, addTokenRouter, sendRouter, - transactionsRouter, changeTokenCollectionRouter); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionDetailViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionDetailViewModel.java index b9213586829..72dd7987b7a 100644 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionDetailViewModel.java +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionDetailViewModel.java @@ -1,15 +1,13 @@ package com.asfoundation.wallet.viewmodel; -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; import android.content.Context; -import android.content.Intent; import android.net.Uri; -import android.support.annotation.Nullable; import android.text.TextUtils; -import com.asf.wallet.R; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import com.asf.wallet.BuildConfig; import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.RawTransaction; import com.asfoundation.wallet.entity.Wallet; import com.asfoundation.wallet.interact.FindDefaultNetworkInteract; import com.asfoundation.wallet.interact.FindDefaultWalletInteract; @@ -17,6 +15,7 @@ import com.asfoundation.wallet.transactions.Operation; import com.asfoundation.wallet.transactions.Transaction; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; public class TransactionDetailViewModel extends BaseViewModel { @@ -24,20 +23,26 @@ public class TransactionDetailViewModel extends BaseViewModel { private final MutableLiveData defaultNetwork = new MutableLiveData<>(); private final MutableLiveData defaultWallet = new MutableLiveData<>(); + private final CompositeDisposable disposables; TransactionDetailViewModel(FindDefaultNetworkInteract findDefaultNetworkInteract, FindDefaultWalletInteract findDefaultWalletInteract, - ExternalBrowserRouter externalBrowserRouter) { + ExternalBrowserRouter externalBrowserRouter, CompositeDisposable compositeDisposable) { this.externalBrowserRouter = externalBrowserRouter; - - findDefaultNetworkInteract.find() + this.disposables = compositeDisposable; + disposables.add(findDefaultNetworkInteract.find() .observeOn(AndroidSchedulers.mainThread()) .subscribe(defaultNetwork::postValue, t -> { - }); - disposable = findDefaultWalletInteract.find() + })); + disposables.add(findDefaultWalletInteract.find() .observeOn(AndroidSchedulers.mainThread()) .subscribe(defaultWallet::postValue, t -> { - }); + })); + } + + @Override protected void onCleared() { + disposables.clear(); + super.onCleared(); } public LiveData defaultNetwork() { @@ -51,18 +56,6 @@ public void showMoreDetails(Context context, Operation transaction) { } } - public void shareTransactionDetail(Context context, Operation operation) { - Uri shareUri = buildEtherscanUri(operation); - if (shareUri != null) { - Intent sharingIntent = new Intent(Intent.ACTION_SEND); - sharingIntent.setType("text/plain"); - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, - context.getString(R.string.subject_transaction_detail)); - sharingIntent.putExtra(Intent.EXTRA_TEXT, shareUri.toString()); - context.startActivity(Intent.createChooser(sharingIntent, "Share via")); - } - } - @Nullable private Uri buildEtherscanUri(Operation operation) { NetworkInfo networkInfo = defaultNetwork.getValue(); if (networkInfo != null && !TextUtils.isEmpty(networkInfo.etherscanUrl)) { @@ -78,4 +71,20 @@ public LiveData defaultWallet() { return defaultWallet; } + public void showMoreDetailsBds(Context context, Transaction transaction) { + Uri uri = buildBdsUri(transaction); + if (uri != null) { + externalBrowserRouter.open(context, uri); + } + } + + private Uri buildBdsUri(Transaction transaction) { + NetworkInfo networkInfo = defaultNetwork.getValue(); + String url = networkInfo.chainId == 3 ? BuildConfig.TRANSACTION_DETAILS_HOST_ROPSTEN + : BuildConfig.TRANSACTION_DETAILS_HOST; + return Uri.parse(url) + .buildUpon() + .appendEncodedPath(transaction.getTransactionId()) + .build(); + } } diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionDetailViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionDetailViewModelFactory.java index 57502b4f4d6..f5d27ad34a5 100644 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionDetailViewModelFactory.java +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionDetailViewModelFactory.java @@ -1,28 +1,31 @@ package com.asfoundation.wallet.viewmodel; -import android.arch.lifecycle.ViewModel; -import android.arch.lifecycle.ViewModelProvider; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; import com.asfoundation.wallet.interact.FindDefaultNetworkInteract; import com.asfoundation.wallet.interact.FindDefaultWalletInteract; import com.asfoundation.wallet.router.ExternalBrowserRouter; +import io.reactivex.disposables.CompositeDisposable; public class TransactionDetailViewModelFactory implements ViewModelProvider.Factory { private final FindDefaultNetworkInteract findDefaultNetworkInteract; private final FindDefaultWalletInteract findDefaultWalletInteract; private final ExternalBrowserRouter externalBrowserRouter; + private final CompositeDisposable compositeDisposable; public TransactionDetailViewModelFactory(FindDefaultNetworkInteract findDefaultNetworkInteract, FindDefaultWalletInteract findDefaultWalletInteract, - ExternalBrowserRouter externalBrowserRouter) { + ExternalBrowserRouter externalBrowserRouter, CompositeDisposable compositeDisposable) { this.findDefaultNetworkInteract = findDefaultNetworkInteract; this.findDefaultWalletInteract = findDefaultWalletInteract; this.externalBrowserRouter = externalBrowserRouter; + this.compositeDisposable = compositeDisposable; } @NonNull @Override public T create(@NonNull Class modelClass) { return (T) new TransactionDetailViewModel(findDefaultNetworkInteract, findDefaultWalletInteract, - externalBrowserRouter); + externalBrowserRouter, compositeDisposable); } } diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionsViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionsViewModel.java index 5373143603f..ed4fa1ecaa3 100644 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionsViewModel.java +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionsViewModel.java @@ -1,102 +1,101 @@ package com.asfoundation.wallet.viewmodel; -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; import android.content.Context; import android.net.Uri; import android.os.Handler; import android.text.format.DateUtils; +import android.util.Pair; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import com.appcoins.wallet.gamification.repository.Levels; import com.asfoundation.wallet.C; +import com.asfoundation.wallet.billing.analytics.WalletsAnalytics; +import com.asfoundation.wallet.billing.analytics.WalletsEventSender; +import com.asfoundation.wallet.entity.Balance; import com.asfoundation.wallet.entity.ErrorEnvelope; +import com.asfoundation.wallet.entity.GlobalBalance; import com.asfoundation.wallet.entity.NetworkInfo; -import com.asfoundation.wallet.entity.RawTransaction; import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.interact.DefaultTokenProvider; -import com.asfoundation.wallet.interact.FetchTransactionsInteract; -import com.asfoundation.wallet.interact.FindDefaultNetworkInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.interact.GetDefaultWalletBalance; -import com.asfoundation.wallet.router.AirdropRouter; -import com.asfoundation.wallet.router.ExternalBrowserRouter; -import com.asfoundation.wallet.router.ManageWalletsRouter; -import com.asfoundation.wallet.router.MyAddressRouter; -import com.asfoundation.wallet.router.MyTokensRouter; -import com.asfoundation.wallet.router.SendRouter; -import com.asfoundation.wallet.router.SettingsRouter; -import com.asfoundation.wallet.router.TransactionDetailRouter; +import com.asfoundation.wallet.interact.TransactionViewInteract; +import com.asfoundation.wallet.navigator.TransactionViewNavigator; +import com.asfoundation.wallet.promotions.PromotionNotification; +import com.asfoundation.wallet.referrals.CardNotification; +import com.asfoundation.wallet.referrals.InviteFriendsActivity; +import com.asfoundation.wallet.support.SupportInteractor; import com.asfoundation.wallet.transactions.Transaction; -import com.asfoundation.wallet.transactions.TransactionsMapper; -import com.asfoundation.wallet.ui.iab.AppcoinsOperationsDataSaver; +import com.asfoundation.wallet.transactions.TransactionsAnalytics; +import com.asfoundation.wallet.ui.AppcoinsApps; +import com.asfoundation.wallet.ui.appcoins.applications.AppcoinsApplication; +import com.asfoundation.wallet.ui.iab.FiatValue; +import com.asfoundation.wallet.ui.widget.entity.TransactionsModel; +import com.asfoundation.wallet.ui.widget.holder.ApplicationClickAction; +import com.asfoundation.wallet.ui.widget.holder.CardNotificationAction; +import com.asfoundation.wallet.util.CurrencyFormatUtils; +import com.asfoundation.wallet.util.WalletCurrency; +import io.reactivex.Completable; import io.reactivex.Observable; -import io.reactivex.Scheduler; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import java.util.List; -import java.util.Map; +import io.reactivex.subjects.PublishSubject; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.concurrent.TimeUnit; public class TransactionsViewModel extends BaseViewModel { - private static final long GET_BALANCE_INTERVAL = 10 * DateUtils.SECOND_IN_MILLIS; - private static final long FETCH_TRANSACTIONS_INTERVAL = 12 * DateUtils.SECOND_IN_MILLIS; + private static final long GET_BALANCE_INTERVAL = 30 * DateUtils.SECOND_IN_MILLIS; + private static final long FETCH_TRANSACTIONS_INTERVAL = 30 * DateUtils.SECOND_IN_MILLIS; + private static final int FIAT_SCALE = 2; + private static final BigDecimal MINUS_ONE = new BigDecimal("-1"); private final MutableLiveData defaultNetwork = new MutableLiveData<>(); private final MutableLiveData defaultWallet = new MutableLiveData<>(); - private final MutableLiveData> transactions = new MutableLiveData<>(); - private final MutableLiveData> defaultWalletBalance = new MutableLiveData<>(); - private final FindDefaultNetworkInteract findDefaultNetworkInteract; - private final FindDefaultWalletInteract findDefaultWalletInteract; - private final FetchTransactionsInteract fetchTransactionsInteract; - private final ManageWalletsRouter manageWalletsRouter; - private final SettingsRouter settingsRouter; - private final SendRouter sendRouter; - private final TransactionDetailRouter transactionDetailRouter; - private final MyAddressRouter myAddressRouter; - private final MyTokensRouter myTokensRouter; - private final ExternalBrowserRouter externalBrowserRouter; - private final CompositeDisposable disposables; - private final DefaultTokenProvider defaultTokenProvider; - private final GetDefaultWalletBalance getDefaultWalletBalance; - private final TransactionsMapper transactionsMapper; - private Handler handler = new Handler(); + private final MutableLiveData transactionsModel = new MutableLiveData<>(); + private final MutableLiveData dismissNotification = new MutableLiveData<>(); + private final MutableLiveData showNotification = new MutableLiveData<>(); + private final MutableLiveData defaultWalletBalance = new MutableLiveData<>(); + private final MutableLiveData gamificationMaxBonus = new MutableLiveData<>(); + private final MutableLiveData fetchTransactionsError = new MutableLiveData<>(); + private final MutableLiveData unreadMessages = new MutableLiveData<>(); + private final MutableLiveData shareApp = new MutableLiveData<>(); + private final AppcoinsApps applications; + private final TransactionsAnalytics analytics; + private final TransactionViewNavigator transactionViewNavigator; + private final TransactionViewInteract transactionViewInteract; + private final SupportInteractor supportInteractor; + private final Handler handler = new Handler(); + private final WalletsEventSender walletsEventSender; + private final PublishSubject topUpClicks = PublishSubject.create(); + private final CurrencyFormatUtils formatter; + private CompositeDisposable disposables; + private final Runnable startGlobalBalanceTask = this::getGlobalBalance; + private boolean hasTransactions = false; + private Disposable fetchTransactionsDisposable; private final Runnable startFetchTransactionsTask = () -> this.fetchTransactions(false); - private final Runnable startGetBalanceTask = this::getBalance; - private final AirdropRouter airdropRouter; - private final AppcoinsOperationsDataSaver operationsDataSaver; - - TransactionsViewModel(FindDefaultNetworkInteract findDefaultNetworkInteract, - FindDefaultWalletInteract findDefaultWalletInteract, - FetchTransactionsInteract fetchTransactionsInteract, ManageWalletsRouter manageWalletsRouter, - SettingsRouter settingsRouter, SendRouter sendRouter, - TransactionDetailRouter transactionDetailRouter, MyAddressRouter myAddressRouter, - MyTokensRouter myTokensRouter, ExternalBrowserRouter externalBrowserRouter, - DefaultTokenProvider defaultTokenProvider, GetDefaultWalletBalance getDefaultWalletBalance, - TransactionsMapper transactionsMapper, AirdropRouter airdropRouter, - AppcoinsOperationsDataSaver operationsDataSaver) { - this.findDefaultNetworkInteract = findDefaultNetworkInteract; - this.findDefaultWalletInteract = findDefaultWalletInteract; - this.fetchTransactionsInteract = fetchTransactionsInteract; - this.manageWalletsRouter = manageWalletsRouter; - this.settingsRouter = settingsRouter; - this.sendRouter = sendRouter; - this.transactionDetailRouter = transactionDetailRouter; - this.myAddressRouter = myAddressRouter; - this.myTokensRouter = myTokensRouter; - this.externalBrowserRouter = externalBrowserRouter; - this.defaultTokenProvider = defaultTokenProvider; - this.getDefaultWalletBalance = getDefaultWalletBalance; - this.transactionsMapper = transactionsMapper; - this.airdropRouter = airdropRouter; - this.operationsDataSaver = operationsDataSaver; - disposables = new CompositeDisposable(); + + TransactionsViewModel(AppcoinsApps applications, TransactionsAnalytics analytics, + TransactionViewNavigator transactionViewNavigator, + TransactionViewInteract transactionViewInteract, SupportInteractor supportInteractor, + WalletsEventSender walletsEventSender, CurrencyFormatUtils formatter) { + this.applications = applications; + this.analytics = analytics; + this.transactionViewNavigator = transactionViewNavigator; + this.transactionViewInteract = transactionViewInteract; + this.supportInteractor = supportInteractor; + this.walletsEventSender = walletsEventSender; + this.formatter = formatter; + this.disposables = new CompositeDisposable(); } @Override protected void onCleared() { super.onCleared(); - + hasTransactions = false; if (!disposables.isDisposed()) { disposables.dispose(); } handler.removeCallbacks(startFetchTransactionsTask); - handler.removeCallbacks(startGetBalanceTask); + handler.removeCallbacks(startGlobalBalanceTask); } public LiveData defaultNetwork() { @@ -107,109 +106,360 @@ public LiveData defaultWallet() { return defaultWallet; } - public LiveData> transactions() { - return transactions; + public LiveData transactionsModel() { + return transactionsModel; } - public MutableLiveData> defaultWalletBalance() { + public LiveData dismissNotification() { + return dismissNotification; + } + + public MutableLiveData getDefaultWalletBalance() { return defaultWalletBalance; } public void prepare() { + if (disposables.isDisposed()) { + disposables = new CompositeDisposable(); + } progress.postValue(true); - disposables.add(findDefaultNetworkInteract.find() + disposables.add(transactionViewInteract.findNetwork() .subscribe(this::onDefaultNetwork, this::onError)); + disposables.add(transactionViewInteract.hasPromotionUpdate() + .subscribeOn(Schedulers.io()) + .subscribe(showNotification::postValue, this::onError)); + disposables.add(transactionViewInteract.getUserLevel() + .subscribeOn(Schedulers.io()) + .flatMap(userLevel -> transactionViewInteract.findWallet() + .subscribeOn(Schedulers.io()) + .map(wallet -> { + registerSupportUser(userLevel, wallet.address); + return true; + })) + .subscribe(wallet -> { + }, this::onError)); + handleTopUpClicks(); + } + + public void handleUnreadConversationCount() { + disposables.add(supportInteractor.getUnreadConversationCountListener() + .subscribeOn(AndroidSchedulers.mainThread()) + .doOnNext(this::updateIntercomAnimation) + .subscribe()); + } + + public void updateConversationCount() { + disposables.add(supportInteractor.getUnreadConversationCount() + .subscribeOn(AndroidSchedulers.mainThread()) + .doOnNext(this::updateIntercomAnimation) + .subscribe()); + } + + private void updateIntercomAnimation(Integer count) { + unreadMessages.setValue(count != null && count != 0); + } + + private Completable publishMaxBonus() { + if (fetchTransactionsError.getValue() != null) { + return Completable.fromAction( + () -> fetchTransactionsError.postValue(fetchTransactionsError.getValue())); + } + return transactionViewInteract.getLevels() + .subscribeOn(Schedulers.io()) + .flatMap(levels -> { + if (levels.getStatus() + .equals(Levels.Status.OK)) { + return Single.just(levels.getList() + .get(levels.getList() + .size() - 1) + .getBonus()); + } + return Single.error(new IllegalStateException(levels.getStatus() + .name())); + }) + .doOnSuccess(fetchTransactionsError::postValue) + .ignoreElement(); } public void fetchTransactions(boolean shouldShowProgress) { handler.removeCallbacks(startFetchTransactionsTask); progress.postValue(shouldShowProgress); - /*For specific address use: new Wallet("0x60f7a1cbc59470b74b1df20b133700ec381f15d3")*/ - Observable> fetch = fetchTransactionsInteract.fetch(defaultWallet.getValue()) - .flatMapSingle(rawTransactions -> transactionsMapper.map(rawTransactions)).observeOn( - AndroidSchedulers.mainThread()); - disposables.add( - fetch.subscribe(this::onTransactions, this::onError, this::onTransactionsFetchCompleted)); + if (fetchTransactionsDisposable != null && !fetchTransactionsDisposable.isDisposed()) { + fetchTransactionsDisposable.dispose(); + } + + fetchTransactionsDisposable = + transactionViewInteract.fetchTransactions(defaultWallet.getValue()) + .flatMapSingle(transactions -> transactionViewInteract.getCardNotifications() + .subscribeOn(Schedulers.io()) + .onErrorReturnItem(Collections.emptyList()) + .flatMap(notifications -> applications.getApps() + .onErrorReturnItem(Collections.emptyList()) + .map(applications -> new TransactionsModel(transactions, notifications, + applications)))) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .flatMapCompletable( + transactionsModel -> publishMaxBonus().observeOn(AndroidSchedulers.mainThread()) + .andThen(onTransactionModel(transactionsModel)) + .andThen(Completable.fromAction(this::onTransactionsFetchCompleted))) + .onErrorResumeNext(throwable -> publishMaxBonus()) + .observeOn(AndroidSchedulers.mainThread()) + .doAfterTerminate(transactionViewInteract::stopTransactionFetch) + .subscribe(() -> { + }, this::onError); + disposables.add(fetchTransactionsDisposable); + } + + private void getGlobalBalance() { + disposables.add(Observable.zip(getAppcBalance(), getCreditsBalance(), getEthereumBalance(), + this::updateWalletValue) + .subscribe(globalBalance -> { + handler.removeCallbacks(startGlobalBalanceTask); + handler.postDelayed(startGlobalBalanceTask, GET_BALANCE_INTERVAL); + }, Throwable::printStackTrace)); + } + + private GlobalBalance updateWalletValue(Pair tokenBalance, + Pair creditsBalance, Pair ethereumBalance) { + String fiatValue = ""; + BigDecimal sumFiat = sumFiat(tokenBalance.second.getAmount(), creditsBalance.second.getAmount(), + ethereumBalance.second.getAmount()); + if (sumFiat.compareTo(MINUS_ONE) > 0) { + fiatValue = formatter.formatCurrency(sumFiat, WalletCurrency.FIAT); + } + GlobalBalance currentGlobalBalance = defaultWalletBalance.getValue(); + GlobalBalance newGlobalBalance = + new GlobalBalance(tokenBalance.first, creditsBalance.first, ethereumBalance.first, + tokenBalance.second.getSymbol(), fiatValue, shouldShow(tokenBalance, 0.01), + shouldShow(creditsBalance, 0.01), shouldShow(ethereumBalance, 0.0001)); + if (currentGlobalBalance != null) { + if (!currentGlobalBalance.equals(newGlobalBalance)) { + defaultWalletBalance.postValue(newGlobalBalance); + } + } else { + defaultWalletBalance.postValue(newGlobalBalance); + } + return newGlobalBalance; + } + + private Observable> getAppcBalance() { + return transactionViewInteract.getAppcBalance(); + } + + private Observable> getEthereumBalance() { + return transactionViewInteract.getEthereumBalance(); } - private void getBalance() { - disposables.add(getDefaultWalletBalance.get(defaultWallet.getValue()) - .subscribe(values -> { - defaultWalletBalance.postValue(values); - handler.removeCallbacks(startGetBalanceTask); - handler.postDelayed(startGetBalanceTask, GET_BALANCE_INTERVAL); - }, throwable -> throwable.printStackTrace())); + private Observable> getCreditsBalance() { + return transactionViewInteract.getCreditsBalance(); + } + + private boolean shouldShow(Pair balance, Double threshold) { + return balance.first.getStringValue() + .length() > 0 + && Double.parseDouble(balance.first.getStringValue()) >= threshold + && (balance.second.getAmount() + .compareTo(MINUS_ONE) > 0) + && balance.second.getAmount() + .doubleValue() >= threshold; + } + + private BigDecimal sumFiat(BigDecimal appcoinsFiatValue, BigDecimal creditsFiatValue, + BigDecimal etherFiatValue) { + BigDecimal fiatSum = MINUS_ONE; + if (appcoinsFiatValue.compareTo(MINUS_ONE) > 0) { + fiatSum = appcoinsFiatValue; + } + + if (creditsFiatValue.compareTo(MINUS_ONE) > 0) { + if (fiatSum.compareTo(MINUS_ONE) > 0) { + fiatSum = fiatSum.add(creditsFiatValue); + } else { + fiatSum = creditsFiatValue; + } + } + + if (etherFiatValue.compareTo(MINUS_ONE) > 0) { + if (fiatSum.compareTo(MINUS_ONE) > 0) { + fiatSum = fiatSum.add(etherFiatValue); + } else { + fiatSum = etherFiatValue; + } + } + return fiatSum; } private void onDefaultNetwork(NetworkInfo networkInfo) { defaultNetwork.postValue(networkInfo); - disposables.add(findDefaultWalletInteract.find() + disposables.add(transactionViewInteract.findWallet() + .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::onDefaultWallet, this::onError)); } private void onDefaultWallet(Wallet wallet) { defaultWallet.setValue(wallet); - getBalance(); + getGlobalBalance(); fetchTransactions(true); } - private void onTransactions(List transactions) { - this.transactions.setValue(transactions); - Boolean last = progress.getValue(); - if (transactions != null && transactions.size() > 0 && last != null && last) { - progress.postValue(true); - } + private Completable onTransactionModel(TransactionsModel transactionsModel) { + return Completable.fromAction(() -> { + transactionsModel.getTransactions(); + hasTransactions = !transactionsModel.getTransactions() + .isEmpty() || hasTransactions; + this.transactionsModel.setValue(transactionsModel); + Boolean last = progress.getValue(); + if (transactionsModel.getTransactions() + .size() > 0 && last != null && last) { + progress.postValue(true); + } + }); } private void onTransactionsFetchCompleted() { progress.postValue(false); - List transactions = this.transactions.getValue(); - if (transactions == null || transactions.size() == 0) { + if (!hasTransactions) { error.postValue(new ErrorEnvelope(C.ErrorCode.EMPTY_COLLECTION, "empty collection")); } handler.postDelayed(startFetchTransactionsTask, FETCH_TRANSACTIONS_INTERVAL); } - public void showWallets(Context context) { - manageWalletsRouter.open(context, false); - } - public void showSettings(Context context) { - settingsRouter.open(context); + transactionViewNavigator.openSettings(context); } public void showSend(Context context) { - defaultTokenProvider.getDefaultToken() - .doOnSuccess(defaultToken -> sendRouter.open(context, defaultToken)) - .subscribe(); + transactionViewNavigator.openSendView(context); } public void showDetails(Context context, Transaction transaction) { - transactionDetailRouter.open(context, transaction); + transactionViewNavigator.openTransactionsDetailView(context, transaction); } public void showMyAddress(Context context) { - myAddressRouter.open(context, defaultWallet.getValue()); + transactionViewNavigator.openMyAddressView(context, defaultWallet.getValue()); } public void showTokens(Context context) { - myTokensRouter.open(context, defaultWallet.getValue()); + transactionViewNavigator.openTokensView(context); } public void pause() { + if (!disposables.isDisposed()) { + disposables.dispose(); + } handler.removeCallbacks(startFetchTransactionsTask); - handler.removeCallbacks(startGetBalanceTask); + handler.removeCallbacks(startGlobalBalanceTask); + } + + public void onAppClick(AppcoinsApplication appcoinsApplication, + ApplicationClickAction applicationClickAction, Context context) { + String url = "https://" + appcoinsApplication.getUniqueName() + ".en.aptoide.com/"; + switch (applicationClickAction) { + case SHARE: + shareApp.setValue(url); + break; + case CLICK: + default: + transactionViewNavigator.navigateToBrowser(context, Uri.parse(url)); + analytics.openApp(appcoinsApplication.getUniqueName(), + appcoinsApplication.getPackageName()); + } + } + + public void showTopApps(Context context) { + transactionViewNavigator.navigateToBrowser(context, + Uri.parse("https://en.aptoide.com/store/bds-store/group/group-10867")); + } + + public MutableLiveData shouldShowPromotionsNotification() { + return showNotification; + } + + public void showTopUp(Context context) { + topUpClicks.onNext(context); + } + + public MutableLiveData gamificationMaxBonus() { + return gamificationMaxBonus; + } + + public MutableLiveData shareApp() { + return shareApp; + } + + public MutableLiveData onFetchTransactionsError() { + return fetchTransactionsError; + } + + public MutableLiveData getUnreadMessages() { + return unreadMessages; + } + + public void navigateToPromotions(Context context) { + transactionViewNavigator.openPromotions(context); + } + + public void onNotificationClick(CardNotification cardNotification, + CardNotificationAction cardNotificationAction, Context context) { + switch (cardNotificationAction) { + case DISMISS: + dismissNotification(cardNotification); + break; + case DISCOVER: + transactionViewNavigator.navigateToBrowser(context, + Uri.parse(InviteFriendsActivity.APTOIDE_TOP_APPS_URL)); + break; + case UPDATE: + transactionViewNavigator.openIntent(context, + transactionViewInteract.retrieveUpdateIntent()); + dismissNotification(cardNotification); + break; + case BACKUP: + Wallet wallet = defaultWallet.getValue(); + if (wallet != null && wallet.address != null) { + transactionViewNavigator.navigateToBackup(context, wallet.address); + walletsEventSender.sendCreateBackupEvent(WalletsAnalytics.ACTION_CREATE, + WalletsAnalytics.CONTEXT_CARD, WalletsAnalytics.STATUS_SUCCESS); + } + break; + case DETAILS_URL: + if (cardNotification instanceof PromotionNotification) { + String url = ((PromotionNotification) cardNotification).getDetailsLink(); + transactionViewNavigator.navigateToBrowser(context, Uri.parse(url)); + } + break; + case NONE: + break; + } + } + + private void dismissNotification(CardNotification cardNotification) { + disposables.add(transactionViewInteract.dismissNotification(cardNotification) + .subscribeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> dismissNotification.postValue(cardNotification), this::onError)); + } + + public void showSupportScreen(boolean fromNotification) { + if (fromNotification) { + supportInteractor.displayConversationListOrChat(); + } else { + supportInteractor.displayChatScreen(); + } } - public void openDeposit(Context context, Uri uri) { - externalBrowserRouter.open(context, uri); + private void registerSupportUser(Integer level, String walletAddress) { + supportInteractor.registerUser(level, walletAddress); } - public void showAirDrop(Context context) { - airdropRouter.open(context); + private void handleTopUpClicks() { + disposables.add(topUpClicks.throttleFirst(1, TimeUnit.SECONDS) + .doOnNext(transactionViewNavigator::openTopUp) + .subscribe()); } - public void onLearnMoreClick(Context context, Uri uri) { - openDeposit(context, uri); + public void clearShareApp() { + shareApp.setValue(null); } } diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionsViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionsViewModelFactory.java deleted file mode 100644 index 7dd20e766cc..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionsViewModelFactory.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.arch.lifecycle.ViewModel; -import android.arch.lifecycle.ViewModelProvider; -import android.support.annotation.NonNull; -import com.asfoundation.wallet.interact.DefaultTokenProvider; -import com.asfoundation.wallet.interact.FetchTransactionsInteract; -import com.asfoundation.wallet.interact.FindDefaultNetworkInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.interact.GetDefaultWalletBalance; -import com.asfoundation.wallet.router.AirdropRouter; -import com.asfoundation.wallet.router.ExternalBrowserRouter; -import com.asfoundation.wallet.router.ManageWalletsRouter; -import com.asfoundation.wallet.router.MyAddressRouter; -import com.asfoundation.wallet.router.MyTokensRouter; -import com.asfoundation.wallet.router.SendRouter; -import com.asfoundation.wallet.router.SettingsRouter; -import com.asfoundation.wallet.router.TransactionDetailRouter; -import com.asfoundation.wallet.transactions.TransactionsMapper; -import com.asfoundation.wallet.ui.iab.AppcoinsOperationsDataSaver; - -public class TransactionsViewModelFactory implements ViewModelProvider.Factory { - - private final FindDefaultNetworkInteract findDefaultNetworkInteract; - private final FindDefaultWalletInteract findDefaultWalletInteract; - private final FetchTransactionsInteract fetchTransactionsInteract; - private final ManageWalletsRouter manageWalletsRouter; - private final SettingsRouter settingsRouter; - private final SendRouter sendRouter; - private final TransactionDetailRouter transactionDetailRouter; - private final MyAddressRouter myAddressRouter; - private final MyTokensRouter myTokensRouter; - private final ExternalBrowserRouter externalBrowserRouter; - private final DefaultTokenProvider defaultTokenProvider; - private final GetDefaultWalletBalance getDefaultWalletBalance; - private final TransactionsMapper transactionsMapper; - private final AirdropRouter airdropRouter; - private final AppcoinsOperationsDataSaver operationsDataSaver; - - public TransactionsViewModelFactory(FindDefaultNetworkInteract findDefaultNetworkInteract, - FindDefaultWalletInteract findDefaultWalletInteract, - FetchTransactionsInteract fetchTransactionsInteract, ManageWalletsRouter manageWalletsRouter, - SettingsRouter settingsRouter, SendRouter sendRouter, - TransactionDetailRouter transactionDetailRouter, MyAddressRouter myAddressRouter, - MyTokensRouter myTokensRouter, ExternalBrowserRouter externalBrowserRouter, - DefaultTokenProvider defaultTokenProvider, GetDefaultWalletBalance getDefaultWalletBalance, - TransactionsMapper transactionsMapper, AirdropRouter airdropRouter, - AppcoinsOperationsDataSaver operationsDataSaver) { - this.findDefaultNetworkInteract = findDefaultNetworkInteract; - this.findDefaultWalletInteract = findDefaultWalletInteract; - this.fetchTransactionsInteract = fetchTransactionsInteract; - this.manageWalletsRouter = manageWalletsRouter; - this.settingsRouter = settingsRouter; - this.sendRouter = sendRouter; - this.transactionDetailRouter = transactionDetailRouter; - this.myAddressRouter = myAddressRouter; - this.myTokensRouter = myTokensRouter; - this.externalBrowserRouter = externalBrowserRouter; - this.defaultTokenProvider = defaultTokenProvider; - this.getDefaultWalletBalance = getDefaultWalletBalance; - this.transactionsMapper = transactionsMapper; - this.airdropRouter = airdropRouter; - this.operationsDataSaver = operationsDataSaver; - } - - @NonNull @Override public T create(@NonNull Class modelClass) { - return (T) new TransactionsViewModel(findDefaultNetworkInteract, findDefaultWalletInteract, - fetchTransactionsInteract, manageWalletsRouter, settingsRouter, sendRouter, - transactionDetailRouter, myAddressRouter, myTokensRouter, externalBrowserRouter, - defaultTokenProvider, getDefaultWalletBalance, transactionsMapper, airdropRouter, - operationsDataSaver); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionsViewModelFactory.kt b/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionsViewModelFactory.kt new file mode 100644 index 00000000000..ff588f430c0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/viewmodel/TransactionsViewModelFactory.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.asfoundation.wallet.billing.analytics.WalletsEventSender +import com.asfoundation.wallet.interact.TransactionViewInteract +import com.asfoundation.wallet.navigator.TransactionViewNavigator +import com.asfoundation.wallet.support.SupportInteractor +import com.asfoundation.wallet.transactions.TransactionsAnalytics +import com.asfoundation.wallet.ui.AppcoinsApps +import com.asfoundation.wallet.util.CurrencyFormatUtils + +class TransactionsViewModelFactory(private val applications: AppcoinsApps, + private val analytics: TransactionsAnalytics, + private val transactionViewNavigator: TransactionViewNavigator, + private val transactionViewInteract: TransactionViewInteract, + private val walletsEventSender: WalletsEventSender, + private val supportInteractor: SupportInteractor, + private val formatter: CurrencyFormatUtils) : + ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return TransactionsViewModel(applications, analytics, transactionViewNavigator, + transactionViewInteract, supportInteractor, walletsEventSender, formatter) as T + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/WalletsViewModel.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/WalletsViewModel.java deleted file mode 100644 index 613074b08ce..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/WalletsViewModel.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.app.Activity; -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; -import android.content.Context; -import android.text.TextUtils; -import com.asfoundation.wallet.C; -import com.asfoundation.wallet.entity.ErrorEnvelope; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.interact.AddTokenInteract; -import com.asfoundation.wallet.interact.CreateWalletInteract; -import com.asfoundation.wallet.interact.DefaultTokenProvider; -import com.asfoundation.wallet.interact.DeleteWalletInteract; -import com.asfoundation.wallet.interact.ExportWalletInteract; -import com.asfoundation.wallet.interact.FetchWalletsInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.interact.SetDefaultWalletInteract; -import com.asfoundation.wallet.router.ImportWalletRouter; -import com.asfoundation.wallet.router.TransactionsRouter; -import com.crashlytics.android.Crashlytics; - -import static com.asfoundation.wallet.C.IMPORT_REQUEST_CODE; - -public class WalletsViewModel extends BaseViewModel { - - private final CreateWalletInteract createWalletInteract; - private final SetDefaultWalletInteract setDefaultWalletInteract; - private final DeleteWalletInteract deleteWalletInteract; - private final FetchWalletsInteract fetchWalletsInteract; - private final FindDefaultWalletInteract findDefaultWalletInteract; - private final ExportWalletInteract exportWalletInteract; - - private final ImportWalletRouter importWalletRouter; - private final TransactionsRouter transactionsRouter; - - private final MutableLiveData wallets = new MutableLiveData<>(); - private final MutableLiveData defaultWallet = new MutableLiveData<>(); - private final MutableLiveData createdWallet = new MutableLiveData<>(); - private final MutableLiveData createWalletError = new MutableLiveData<>(); - private final MutableLiveData exportedStore = new MutableLiveData<>(); - private final MutableLiveData exportWalletError = new MutableLiveData<>(); - private final MutableLiveData deleteWalletError = new MutableLiveData<>(); - private final AddTokenInteract addTokenInteract; - private final DefaultTokenProvider defaultTokenProvider; - - WalletsViewModel(CreateWalletInteract createWalletInteract, - SetDefaultWalletInteract setDefaultWalletInteract, DeleteWalletInteract deleteWalletInteract, - FetchWalletsInteract fetchWalletsInteract, - FindDefaultWalletInteract findDefaultWalletInteract, - ExportWalletInteract exportWalletInteract, ImportWalletRouter importWalletRouter, - TransactionsRouter transactionsRouter, AddTokenInteract addTokenInteract, - DefaultTokenProvider defaultTokenProvider) { - this.createWalletInteract = createWalletInteract; - this.setDefaultWalletInteract = setDefaultWalletInteract; - this.deleteWalletInteract = deleteWalletInteract; - this.fetchWalletsInteract = fetchWalletsInteract; - this.findDefaultWalletInteract = findDefaultWalletInteract; - this.importWalletRouter = importWalletRouter; - this.exportWalletInteract = exportWalletInteract; - this.transactionsRouter = transactionsRouter; - this.addTokenInteract = addTokenInteract; - this.defaultTokenProvider = defaultTokenProvider; - - fetchWallets(); - } - - public LiveData wallets() { - return wallets; - } - - public LiveData defaultWallet() { - return defaultWallet; - } - - public LiveData createdWallet() { - return createdWallet; - } - - public LiveData createWalletError() { - return createWalletError; - } - - public LiveData exportedStore() { - return exportedStore; - } - - public LiveData exportWalletError() { - return exportWalletError; - } - - public LiveData deleteWalletError() { - return deleteWalletError; - } - - public void setDefaultWallet(Wallet wallet) { - disposable = setDefaultWalletInteract.set(wallet) - .subscribe(() -> onDefaultWalletChanged(wallet), this::onError); - } - - public void deleteWallet(Wallet wallet) { - disposable = deleteWalletInteract.delete(wallet) - .subscribe(this::onFetchWallets, this::onDeleteWalletError); - } - - private void onFetchWallets(Wallet[] items) { - progress.postValue(false); - wallets.postValue(items); - disposable = findDefaultWalletInteract.find() - .subscribe(this::onDefaultWalletChanged, t -> { - }); - } - - private void onDefaultWalletChanged(Wallet wallet) { - progress.postValue(false); - defaultWallet.postValue(wallet); - - addDefaultToken(); - } - - private void addDefaultToken() { - defaultTokenProvider.getDefaultToken() - .flatMapCompletable( - defaultToken -> addTokenInteract.add(defaultToken.address, defaultToken.symbol, - defaultToken.decimals)) - .subscribe(); - } - - public void fetchWallets() { - progress.postValue(true); - disposable = fetchWalletsInteract.fetch() - .subscribe(this::onFetchWallets, this::onError); - } - - public void newWallet() { - progress.setValue(true); - createWalletInteract.create() - .subscribe(account -> { - fetchWallets(); - createdWallet.postValue(account); - }, this::onCreateWalletError); - } - - public void exportWallet(Wallet wallet, String storePassword) { - exportWalletInteract.export(wallet, storePassword) - .subscribe(exportedStore::postValue, this::onExportWalletError); - } - - private void onExportWalletError(Throwable throwable) { - Crashlytics.logException(throwable); - exportWalletError.postValue(new ErrorEnvelope(C.ErrorCode.UNKNOWN, - TextUtils.isEmpty(throwable.getLocalizedMessage()) ? throwable.getMessage() - : throwable.getLocalizedMessage())); - } - - private void onDeleteWalletError(Throwable throwable) { - Crashlytics.logException(throwable); - deleteWalletError.postValue(new ErrorEnvelope(C.ErrorCode.UNKNOWN, - TextUtils.isEmpty(throwable.getLocalizedMessage()) ? throwable.getMessage() - : throwable.getLocalizedMessage())); - } - - private void onCreateWalletError(Throwable throwable) { - throwable.printStackTrace(); - Crashlytics.logException(throwable); - progress.postValue(false); - createWalletError.postValue(new ErrorEnvelope(C.ErrorCode.UNKNOWN, throwable.getMessage())); - } - - public void importWallet(Activity activity) { - importWalletRouter.openForResult(activity, IMPORT_REQUEST_CODE); - } - - public void showTransactions(Context context) { - transactionsRouter.open(context, true); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/viewmodel/WalletsViewModelFactory.java b/app/src/main/java/com/asfoundation/wallet/viewmodel/WalletsViewModelFactory.java deleted file mode 100644 index eab40876bcf..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/viewmodel/WalletsViewModelFactory.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.asfoundation.wallet.viewmodel; - -import android.arch.lifecycle.ViewModel; -import android.arch.lifecycle.ViewModelProvider; -import android.support.annotation.NonNull; -import com.asfoundation.wallet.interact.AddTokenInteract; -import com.asfoundation.wallet.interact.CreateWalletInteract; -import com.asfoundation.wallet.interact.DefaultTokenProvider; -import com.asfoundation.wallet.interact.DeleteWalletInteract; -import com.asfoundation.wallet.interact.ExportWalletInteract; -import com.asfoundation.wallet.interact.FetchWalletsInteract; -import com.asfoundation.wallet.interact.FindDefaultWalletInteract; -import com.asfoundation.wallet.interact.SetDefaultWalletInteract; -import com.asfoundation.wallet.router.ImportWalletRouter; -import com.asfoundation.wallet.router.TransactionsRouter; -import javax.inject.Inject; - -public class WalletsViewModelFactory implements ViewModelProvider.Factory { - - private final CreateWalletInteract createWalletInteract; - private final SetDefaultWalletInteract setDefaultWalletInteract; - private final DeleteWalletInteract deleteWalletInteract; - private final FetchWalletsInteract fetchWalletsInteract; - private final FindDefaultWalletInteract findDefaultWalletInteract; - private final ExportWalletInteract exportWalletInteract; - - private final ImportWalletRouter importWalletRouter; - private final TransactionsRouter transactionsRouter; - private final AddTokenInteract addTokenInteract; - private final DefaultTokenProvider defaultTokenProvider; - - @Inject public WalletsViewModelFactory(CreateWalletInteract createWalletInteract, - SetDefaultWalletInteract setDefaultWalletInteract, DeleteWalletInteract deleteWalletInteract, - FetchWalletsInteract fetchWalletsInteract, - FindDefaultWalletInteract findDefaultWalletInteract, - ExportWalletInteract exportWalletInteract, ImportWalletRouter importWalletRouter, - TransactionsRouter transactionsRouter, AddTokenInteract addTokenInteract, - DefaultTokenProvider defaultTokenProvider) { - this.createWalletInteract = createWalletInteract; - this.setDefaultWalletInteract = setDefaultWalletInteract; - this.deleteWalletInteract = deleteWalletInteract; - this.fetchWalletsInteract = fetchWalletsInteract; - this.findDefaultWalletInteract = findDefaultWalletInteract; - this.exportWalletInteract = exportWalletInteract; - this.importWalletRouter = importWalletRouter; - this.transactionsRouter = transactionsRouter; - this.addTokenInteract = addTokenInteract; - this.defaultTokenProvider = defaultTokenProvider; - } - - @NonNull @Override public T create(@NonNull Class modelClass) { - return (T) new WalletsViewModel(createWalletInteract, setDefaultWalletInteract, - deleteWalletInteract, fetchWalletsInteract, findDefaultWalletInteract, exportWalletInteract, - importWalletRouter, transactionsRouter, addTokenInteract, defaultTokenProvider); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletBlockedActivity.kt b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletBlockedActivity.kt new file mode 100644 index 00000000000..01fc2d67f80 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletBlockedActivity.kt @@ -0,0 +1,81 @@ +package com.asfoundation.wallet.wallet_blocked + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.asf.wallet.R +import com.asfoundation.wallet.ui.BaseActivity +import com.jakewharton.rxbinding2.view.RxView +import dagger.android.AndroidInjection +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.layout_wallet_blocked.* + +class WalletBlockedActivity : BaseActivity(), + WalletBlockedView { + + private lateinit var presenter: WalletBlockedPresenter + + companion object { + @JvmStatic + fun newIntent(context: Context): Intent { + return Intent(context, WalletBlockedActivity::class.java) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.layout_wallet_blocked) + + presenter = + WalletBlockedPresenter(this, + CompositeDisposable(), AndroidSchedulers.mainThread()) + presenter.present() + } + + override fun onResume() { + super.onResume() + sendPageViewEvent() + } + + override fun getDismissCLicks(): Observable { + return RxView.clicks(dismiss_button) + } + + override fun getEmailClicks(): Observable { + return RxView.clicks(blocked_email) + } + + override fun openEmail() { + val intent = Intent(Intent.ACTION_SEND_MULTIPLE) + .apply { + type = "message/rfc822" + putExtra(Intent.EXTRA_SUBJECT, "Blocked wallet") + putExtra(Intent.EXTRA_EMAIL, arrayOf("info@appcoins.io")) + } + startActivity(Intent.createChooser(intent, "Select email application.")) + } + + override fun dismiss() { + closeSuccess() + } + + override fun onBackPressed() { + closeSuccess() + super.onBackPressed() + } + + override fun onDestroy() { + presenter.stop() + super.onDestroy() + } + + private fun closeSuccess() { + setResult(Activity.RESULT_OK, Intent()) + finish() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletBlockedInteract.kt b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletBlockedInteract.kt new file mode 100644 index 00000000000..1b2257391cb --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletBlockedInteract.kt @@ -0,0 +1,19 @@ +package com.asfoundation.wallet.wallet_blocked + +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import io.reactivex.Single +import java.util.concurrent.TimeUnit + +class WalletBlockedInteract( + private val findDefaultWalletInteract: FindDefaultWalletInteract, + private val walletStatusRepository: WalletStatusRepository +) { + + fun isWalletBlocked(): Single { + return findDefaultWalletInteract.find() + .flatMap { walletStatusRepository.isWalletBlocked(it.address) } + .onErrorReturn { false } + .delay(1, TimeUnit.SECONDS) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletBlockedPresenter.kt b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletBlockedPresenter.kt new file mode 100644 index 00000000000..38eaf3fdfed --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletBlockedPresenter.kt @@ -0,0 +1,39 @@ +package com.asfoundation.wallet.wallet_blocked + +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +class WalletBlockedPresenter( + private val view: WalletBlockedView, + private val disposables: CompositeDisposable, + private val viewScheduler: Scheduler +) { + + fun present() { + handleDismissCLicks() + handleEmailClicks() + } + + fun stop() { + disposables.clear() + } + + private fun handleDismissCLicks() { + disposables.add( + view.getDismissCLicks() + .observeOn(viewScheduler) + .doOnNext { view.dismiss() } + .subscribe() + ) + } + + private fun handleEmailClicks() { + disposables.add( + view.getEmailClicks() + .observeOn(viewScheduler) + .doOnNext { view.openEmail() } + .subscribe() + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletBlockedView.kt b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletBlockedView.kt new file mode 100644 index 00000000000..55f7b05aab4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletBlockedView.kt @@ -0,0 +1,12 @@ +package com.asfoundation.wallet.wallet_blocked + +import io.reactivex.Observable + +interface WalletBlockedView { + + fun getDismissCLicks(): Observable + fun getEmailClicks(): Observable + fun dismiss() + fun openEmail() + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletStatusApi.kt b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletStatusApi.kt new file mode 100644 index 00000000000..d6ee26de9cf --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletStatusApi.kt @@ -0,0 +1,12 @@ +package com.asfoundation.wallet.wallet_blocked + +import io.reactivex.Single +import retrofit2.http.GET +import retrofit2.http.Query + +interface WalletStatusApi { + + @GET("transaction/blocked") + fun isWalletBlocked(@Query("wallet") wallet: String): Single + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletStatusRepository.kt b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletStatusRepository.kt new file mode 100644 index 00000000000..4af783c38fa --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletStatusRepository.kt @@ -0,0 +1,14 @@ +package com.asfoundation.wallet.wallet_blocked + +import io.reactivex.Single + +class WalletStatusRepository( + private val api: WalletStatusApi +) { + + fun isWalletBlocked(walletAddress: String): Single { + return api.isWalletBlocked(walletAddress) + .map { it.blocked } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletStatusResponse.kt b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletStatusResponse.kt new file mode 100644 index 00000000000..d4ba5044f15 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_blocked/WalletStatusResponse.kt @@ -0,0 +1,3 @@ +package com.asfoundation.wallet.wallet_blocked + +data class WalletStatusResponse(val blocked: Boolean) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/DeleteKeyListener.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/DeleteKeyListener.kt new file mode 100644 index 00000000000..6f6b18e3744 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/DeleteKeyListener.kt @@ -0,0 +1,20 @@ +package com.asfoundation.wallet.wallet_validation + +import android.view.KeyEvent +import android.view.View +import android.widget.EditText + +class DeleteKeyListener(private val inputTexts: Array, + private val selectedPosition: Int) : View.OnKeyListener { + + override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean { + if (event?.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_DEL) { + inputTexts[selectedPosition].setText("") + if (selectedPosition > 0) { + inputTexts[selectedPosition - 1].requestFocus() + } + return true + } + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/PasteTextWatcher.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/PasteTextWatcher.kt new file mode 100644 index 00000000000..b3dd0bb18e1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/PasteTextWatcher.kt @@ -0,0 +1,72 @@ +package com.asfoundation.wallet.wallet_validation + +import android.content.ClipDescription +import android.content.ClipboardManager +import android.text.Editable +import android.text.TextWatcher +import android.widget.EditText +import org.apache.commons.lang3.StringUtils + +class PasteTextWatcher(private val inputTexts: Array, + private val clipboardManager: ClipboardManager, + private val selectedPosition: Int) : TextWatcher { + + private var isPaste = false + private var isStart = false + private var isDelete = false + private var previousChar = "" + + override fun afterTextChanged(s: Editable?) { + if (isDelete) { + if (selectedPosition > 0) { + inputTexts[selectedPosition - 1].requestFocus() + inputTexts[selectedPosition - 1].setSelection(inputTexts[selectedPosition - 1].length()) + return + } + } + + if (s?.length ?: 0 > 1 && isPaste && isValidPaste()) { + inputTexts[selectedPosition].setText(previousChar) + val text = getTextFromClipboard() + text?.forEachIndexed { index, digit -> + when (index) { + 0, 1, 2, 3, 4, 5 -> inputTexts[index].setText(digit.toString()) + else -> return@forEachIndexed + } + } + } + if (s?.length ?: 0 > 1) { + if (isStart) { + s?.delete(1, 2) + } else { + s?.delete(0, 1) + } + } + + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + isStart = start == 0 + isDelete = (start == 0 && count == 1 && after == 0 && s?.length ?: 0 <= 1) + if (after > 0) { + previousChar = s.toString() + } + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + isPaste = count > 1 + } + + private fun isValidPaste(): Boolean { + return clipboardManager.primaryClipDescription?.hasMimeType( + ClipDescription.MIMETYPE_TEXT_PLAIN) == true && StringUtils.isNumeric( + getTextFromClipboard()) + } + + private fun getTextFromClipboard(): String? { + return clipboardManager.primaryClip?.getItemAt(0) + ?.text?.toString() + ?.replace(Regex("[^\\d.]"), "") + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/ValidationInfo.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/ValidationInfo.kt new file mode 100644 index 00000000000..1544b582351 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/ValidationInfo.kt @@ -0,0 +1,7 @@ +package com.asfoundation.wallet.wallet_validation + +import java.io.Serializable + +data class ValidationInfo(val code1: String, val code2: String, val code3: String, + val code4: String, val code5: String, val code6: String, + val countryCode: String, val phoneNumber: String) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/WalletValidationStatus.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/WalletValidationStatus.kt new file mode 100644 index 00000000000..de97acfbea1 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/WalletValidationStatus.kt @@ -0,0 +1,17 @@ +package com.asfoundation.wallet.wallet_validation + +enum class WalletValidationStatus { + + SUCCESS, + INVALID_INPUT, + INVALID_PHONE, + INVALID_CODE, + DOUBLE_SPENT, + GENERIC_ERROR, + NO_NETWORK, + REGION_NOT_SUPPORTED, + LANDLINE_NOT_SUPPORTED, + EXPIRED_CODE, + TOO_MANY_ATTEMPTS + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/CodeValidationDialogFragment.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/CodeValidationDialogFragment.kt new file mode 100644 index 00000000000..63fe54563ee --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/CodeValidationDialogFragment.kt @@ -0,0 +1,293 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import android.content.ClipboardManager +import android.content.Context +import android.content.Context.CLIPBOARD_SERVICE +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import androidx.fragment.app.Fragment +import com.asf.wallet.R +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.asfoundation.wallet.wallet_validation.DeleteKeyListener +import com.asfoundation.wallet.wallet_validation.PasteTextWatcher +import com.asfoundation.wallet.wallet_validation.ValidationInfo +import com.asfoundation.wallet.wallet_validation.generic.WalletValidationAnalytics +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxbinding2.widget.RxTextView +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.fragment_sms_code.* +import kotlinx.android.synthetic.main.single_sms_input_layout.view.* +import kotlinx.android.synthetic.main.sms_text_input_layout.* +import javax.inject.Inject + + +class CodeValidationDialogFragment : BasePageViewFragment(), CodeValidationDialogView { + + @Inject + lateinit var smsValidationInteract: SmsValidationInteract + + @Inject + lateinit var analytics: WalletValidationAnalytics + + private var walletValidationDialogView: WalletValidationDialogView? = null + private lateinit var presenter: CodeValidationDialogPresenter + private lateinit var fragmentContainer: ViewGroup + private lateinit var clipboard: ClipboardManager + + val countryCode: String by lazy { + if (arguments!!.containsKey(PhoneValidationDialogFragment.COUNTRY_CODE)) { + arguments!!.getString(PhoneValidationDialogFragment.COUNTRY_CODE) + } else { + throw IllegalArgumentException("Country Code not passed") + } + } + + val phoneNumber: String by lazy { + if (arguments!!.containsKey(PhoneValidationDialogFragment.PHONE_NUMBER)) { + arguments!!.getString(PhoneValidationDialogFragment.PHONE_NUMBER) + } else { + throw IllegalArgumentException("Phone Number not passed") + } + } + + private val errorMessage: Int? by lazy { + if (arguments!!.containsKey(ERROR_MESSAGE)) { + arguments!!.getInt(ERROR_MESSAGE) + } else { + null + } + } + + private val validationInfo: ValidationInfo? by lazy { + if (arguments!!.containsKey(VALIDATION_INFO)) { + arguments!!.getSerializable(VALIDATION_INFO) as ValidationInfo + } else { + null + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + clipboard = context!!.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + + presenter = + CodeValidationDialogPresenter(this, walletValidationDialogView, smsValidationInteract, + AndroidSchedulers.mainThread(), Schedulers.io(), countryCode, phoneNumber, + CompositeDisposable(), analytics) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + fragmentContainer = container!! + return inflater.inflate(R.layout.fragment_sms_code, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.present() + } + + override fun onResume() { + super.onResume() + + focusAndShowKeyboard(code_1.code) + } + + override fun setupUI() { + if (errorMessage == null) { + error.visibility = View.INVISIBLE + setButtonState(true) + } else { + error.visibility = View.VISIBLE + error.text = getString(errorMessage!!) + setButtonState(false) + } + + validationInfo?.let { + code_1.code.setText(it.code1) + code_2.code.setText(it.code2) + code_3.code.setText(it.code3) + code_4.code.setText(it.code4) + code_5.code.setText(it.code5) + code_6.code.setText(it.code6) + } + + val inputTexts = + arrayOf(code_1.code, code_2.code, code_3.code, code_4.code, code_5.code, code_6.code) + + code_1.code.addTextChangedListener(PasteTextWatcher(inputTexts, clipboard, 0)) + code_2.code.addTextChangedListener(PasteTextWatcher(inputTexts, clipboard, 1)) + code_3.code.addTextChangedListener(PasteTextWatcher(inputTexts, clipboard, 2)) + code_4.code.addTextChangedListener(PasteTextWatcher(inputTexts, clipboard, 3)) + code_5.code.addTextChangedListener(PasteTextWatcher(inputTexts, clipboard, 4)) + code_6.code.addTextChangedListener(PasteTextWatcher(inputTexts, clipboard, 5)) + code_1.code.setOnKeyListener(DeleteKeyListener(inputTexts, 0)) + code_2.code.setOnKeyListener(DeleteKeyListener(inputTexts, 1)) + code_3.code.setOnKeyListener(DeleteKeyListener(inputTexts, 2)) + code_4.code.setOnKeyListener(DeleteKeyListener(inputTexts, 3)) + code_5.code.setOnKeyListener(DeleteKeyListener(inputTexts, 4)) + code_6.code.setOnKeyListener(DeleteKeyListener(inputTexts, 5)) + } + + override fun clearUI() { + error.visibility = View.INVISIBLE + code_1.code.text = null + code_2.code.text = null + code_3.code.text = null + code_4.code.text = null + code_5.code.text = null + code_6.code.text = null + } + + override fun setButtonState(state: Boolean) { + submit_button.isEnabled = state + } + + override fun getBackClicks() = RxView.clicks(back_button) + + override fun getSubmitClicks(): Observable { + return RxView.clicks(submit_button) + .map { + ValidationInfo(code_1.code.text.toString(), + code_2.code.text.toString(), code_3.code.text.toString(), + code_4.code.text.toString(), code_5.code.text.toString(), + code_6.code.text.toString(), countryCode, + phoneNumber) + } + } + + override fun getResentCodeClicks() = RxView.clicks(resend_code) + + + override fun getFirstChar(): Observable { + return RxTextView.afterTextChangeEvents(code_1.code) + .map { + it.editable() + ?.toString() + } + } + + override fun getSecondChar(): Observable { + return RxTextView.afterTextChangeEvents(code_2.code) + .map { + it.editable() + ?.toString() + } + } + + override fun getThirdChar(): Observable { + return RxTextView.afterTextChangeEvents(code_3.code) + .map { + it.editable() + ?.toString() + } + } + + override fun getFourthChar(): Observable { + return RxTextView.afterTextChangeEvents(code_4.code) + .map { + it.editable() + ?.toString() + } + } + + override fun getFifthChar(): Observable { + return RxTextView.afterTextChangeEvents(code_5.code) + .map { + it.editable() + ?.toString() + } + } + + override fun getSixthChar(): Observable { + return RxTextView.afterTextChangeEvents(code_6.code) + .map { + it.editable() + ?.toString() + } + } + + override fun moveToNextView(current: Int) { + when (current) { + 1 -> code_2.requestFocus() + 2 -> code_3.requestFocus() + 3 -> code_4.requestFocus() + 4 -> code_5.requestFocus() + 5 -> code_6.requestFocus() + } + } + + override fun onDestroy() { + presenter.stop() + super.onDestroy() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + + if (context !is WalletValidationDialogView) { + throw IllegalStateException( + "CodeValidationFragment must be attached to Wallet Validation activity") + } + + walletValidationDialogView = context + } + + override fun onDetach() { + super.onDetach() + walletValidationDialogView = null + } + + override fun hideKeyboard() { + val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.hideSoftInputFromWindow(fragmentContainer.windowToken, 0) + code_6.clearFocus() + } + + companion object { + + internal const val ERROR_MESSAGE = "ERROR_MESSAGE" + internal const val VALIDATION_INFO = "VALIDATION_INFO" + + @JvmStatic + fun newInstance(countryCode: String, phoneNumber: String): Fragment { + val bundle = Bundle().apply { + putString(PhoneValidationDialogFragment.COUNTRY_CODE, countryCode) + putString(PhoneValidationDialogFragment.PHONE_NUMBER, phoneNumber) + } + + return CodeValidationDialogFragment().apply { arguments = bundle } + } + + @JvmStatic + fun newInstance(info: ValidationInfo, errorMessage: Int): Fragment { + val bundle = Bundle().apply { + putString(PhoneValidationDialogFragment.COUNTRY_CODE, info.countryCode) + putString(PhoneValidationDialogFragment.PHONE_NUMBER, info.phoneNumber) + putInt(ERROR_MESSAGE, errorMessage) + putSerializable(VALIDATION_INFO, info) + } + + return CodeValidationDialogFragment().apply { arguments = bundle } + } + + } + + private fun focusAndShowKeyboard(view: EditText) { + view.post { + view.requestFocus() + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.showSoftInput(view, InputMethodManager.SHOW_FORCED) + } + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/CodeValidationDialogPresenter.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/CodeValidationDialogPresenter.kt new file mode 100644 index 00000000000..ffe7f5aeefc --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/CodeValidationDialogPresenter.kt @@ -0,0 +1,137 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.wallet_validation.generic.WalletValidationAnalytics +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.Function6 + +class CodeValidationDialogPresenter( + private val view: CodeValidationDialogView, + private val activity: WalletValidationDialogView?, + private val smsValidationInteract: SmsValidationInteract, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val countryCode: String, + private val phoneNumber: String, + private val disposables: CompositeDisposable, + private val analytics: WalletValidationAnalytics +) { + + fun present() { + view.setupUI() + handleBack() + handleCode() + handleResendCode() + handleValuesChange() + handleSubmit() + } + + private fun handleResendCode() { + disposables.add( + view.getResentCodeClicks() + .doOnNext { + view.clearUI() + } + .subscribeOn(viewScheduler) + .flatMapSingle { + smsValidationInteract.requestValidationCode("+$countryCode$phoneNumber") + .subscribeOn(networkScheduler) + } + .retry() + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleSubmit() { + disposables.add( + view.getSubmitClicks() + .doOnNext { + analytics.sendCodeVerificationEvent("submit") + activity?.showLoading(it) + } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleBack() { + disposables.add( + view.getBackClicks() + .doOnNext { + analytics.sendCodeVerificationEvent("back") + activity?.showPhoneValidationView(countryCode, phoneNumber) + } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleValuesChange() { + disposables.add( + Observable.combineLatest( + view.getFirstChar(), + view.getSecondChar(), + view.getThirdChar(), + view.getFourthChar(), + view.getFifthChar(), + view.getSixthChar(), + Function6 { first: String, second: String, third: String, fourth: String, fifth: String, sixth: String -> + if (isValidInput(first, second, third, fourth, fifth, sixth)) { + view.setButtonState(true) + } else { + view.setButtonState(false) + } + }) + .subscribe({}, { it.printStackTrace() })) + } + + private fun isValidInput(first: String, second: String, third: String, + fourth: String, fifth: String, sixth: String): Boolean { + return first.isNotBlank() && + second.isNotBlank() && + third.isNotBlank() && + fourth.isNotBlank() && + fifth.isNotBlank() && + sixth.isNotBlank() + } + + private fun handleCode() { + disposables.add(view.getFirstChar() + .filter { it.isNotBlank() } + .doOnNext { view.moveToNextView(1) } + .subscribe({}, { it.printStackTrace() }) + ) + + disposables.add(view.getSecondChar() + .filter { it.isNotBlank() } + .doOnNext { view.moveToNextView(2) } + .subscribe({}, { it.printStackTrace() }) + ) + + disposables.add(view.getThirdChar() + .filter { it.isNotBlank() } + .doOnNext { view.moveToNextView(3) } + .subscribe({}, { it.printStackTrace() }) + ) + + disposables.add(view.getFourthChar() + .filter { it.isNotBlank() } + .doOnNext { view.moveToNextView(4) } + .subscribe({}, { it.printStackTrace() }) + ) + + disposables.add(view.getFifthChar() + .filter { it.isNotBlank() } + .doOnNext { view.moveToNextView(5) } + .subscribe({}, { it.printStackTrace() }) + ) + + disposables.add(view.getSixthChar() + .filter { it.isNotBlank() } + .doOnNext { view.hideKeyboard() } + .subscribe({}, { it.printStackTrace() }) + ) + } + + fun stop() = disposables.dispose() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/CodeValidationDialogView.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/CodeValidationDialogView.kt new file mode 100644 index 00000000000..6d117078968 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/CodeValidationDialogView.kt @@ -0,0 +1,36 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import com.asfoundation.wallet.wallet_validation.ValidationInfo +import io.reactivex.Observable + +interface CodeValidationDialogView { + + fun setupUI() + + fun clearUI() + + fun getBackClicks(): Observable + + fun getSubmitClicks(): Observable + + fun getResentCodeClicks(): Observable + + fun getFirstChar(): Observable + + fun getSecondChar(): Observable + + fun getThirdChar(): Observable + + fun getFourthChar(): Observable + + fun getFifthChar(): Observable + + fun getSixthChar(): Observable + + fun moveToNextView(current: Int) + + fun setButtonState(state: Boolean) + + fun hideKeyboard() + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/PhoneValidationDialogFragment.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/PhoneValidationDialogFragment.kt new file mode 100644 index 00000000000..554d550de08 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/PhoneValidationDialogFragment.kt @@ -0,0 +1,189 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import androidx.fragment.app.Fragment +import com.asf.wallet.R +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.wallet_validation.generic.WalletValidationAnalytics +import com.hbb20.CountryCodePicker +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxbinding2.widget.RxTextView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.fragment_phone_validation.* +import javax.inject.Inject + + +class PhoneValidationDialogFragment : DaggerFragment(), + PhoneValidationDialogView { + + @Inject + lateinit var interactor: SmsValidationInteract + + @Inject + lateinit var analytics: WalletValidationAnalytics + + private var walletValidationDialogView: WalletValidationDialogView? = null + private lateinit var presenter: PhoneValidationDialogPresenter + + private var countryCode: String? = null + private var phoneNumber: String? = null + private var errorMessage: Int? = null + + companion object { + + internal const val COUNTRY_CODE = "COUNTRY_CODE" + internal const val PHONE_NUMBER = "PHONE_NUMBER" + internal const val ERROR_MESSAGE = "ERROR_MESSAGE" + + @JvmStatic + fun newInstance(countryCode: String? = null, phoneNumber: String? = null, + errorMessage: Int? = null): Fragment { + val bundle = Bundle().apply { + putString(COUNTRY_CODE, countryCode) + putString(PHONE_NUMBER, phoneNumber) + } + + errorMessage?.let { + bundle.putInt(ERROR_MESSAGE, errorMessage) + } + + return PhoneValidationDialogFragment().apply { arguments = bundle } + } + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + presenter = + PhoneValidationDialogPresenter(this, + walletValidationDialogView, interactor, + AndroidSchedulers.mainThread(), Schedulers.io(), CompositeDisposable(), analytics) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_phone_validation, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (arguments?.containsKey(COUNTRY_CODE) == true) { + countryCode = arguments?.getString(COUNTRY_CODE) + } + if (arguments?.containsKey(PHONE_NUMBER) == true) { + phoneNumber = arguments?.getString(PHONE_NUMBER) + } + if (arguments?.containsKey(ERROR_MESSAGE) == true) { + errorMessage = arguments?.getInt(ERROR_MESSAGE) + } + + presenter.present() + } + + override fun onResume() { + super.onResume() + + presenter.onResume() + focusAndShowKeyboard(phone_number) + } + + override fun setupUI() { + country_code_picker.registerCarrierNumberEditText(phone_number) + country_code_picker.setCustomDialogTextProvider(object : + CountryCodePicker.CustomDialogTextProvider { + override fun getCCPDialogSearchHintText(language: CountryCodePicker.Language?, + defaultSearchHintText: String?) = + defaultSearchHintText ?: "" + + override fun getCCPDialogTitle(language: CountryCodePicker.Language?, defaultTitle: String?) = + getString(R.string.verification_insert_phone_field_country) + + override fun getCCPDialogNoResultACK(language: CountryCodePicker.Language?, + defaultNoResultACK: String?) = defaultNoResultACK ?: "" + }) + + countryCode?.let { + country_code_picker.setCountryForPhoneCode(it.drop(0) + .toInt()) + } + phoneNumber?.let { phone_number.setText(it) } + + errorMessage?.let { setError(it) } + + } + + override fun setError(message: Int) { + phone_number_layout.error = getString(message) + } + + override fun clearError() { + phone_number_layout.error = null + } + + override fun getCountryCode() = Observable.just(country_code_picker.selectedCountryCodeWithPlus) + + + override fun getPhoneNumber(): Observable { + return RxTextView.afterTextChangeEvents(phone_number) + .map { + it.editable() + ?.toString() + } + } + + override fun setButtonState(state: Boolean) { + submit_button.isEnabled = state + } + + override fun getSubmitClicks(): Observable> { + return RxView.clicks(submit_button) + .map { + Pair(country_code_picker.selectedCountryCodeWithPlus, + country_code_picker.fullNumber.substringAfter( + country_code_picker.selectedCountryCode)) + } + } + + override fun getCancelClicks() = RxView.clicks(cancel_button) + + override fun onDestroy() { + presenter.stop() + super.onDestroy() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + + if (context !is WalletValidationDialogView) { + throw IllegalStateException( + "PoaPhoneValidationFragment must be attached to Wallet Validation activity") + } + + walletValidationDialogView = context + } + + override fun onDetach() { + super.onDetach() + walletValidationDialogView = null + } + + private fun focusAndShowKeyboard(view: EditText) { + view.post { + view.requestFocus() + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.showSoftInput(view, InputMethodManager.SHOW_FORCED) + } + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/PhoneValidationDialogPresenter.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/PhoneValidationDialogPresenter.kt new file mode 100644 index 00000000000..db3b384bbd4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/PhoneValidationDialogPresenter.kt @@ -0,0 +1,129 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import androidx.annotation.StringRes +import com.asf.wallet.R +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import com.asfoundation.wallet.wallet_validation.generic.WalletValidationAnalytics +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.BiFunction + +class PhoneValidationDialogPresenter( + private val view: PhoneValidationDialogView, + private val activity: WalletValidationDialogView?, + private val smsValidationInteract: SmsValidationInteract, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val analytics: WalletValidationAnalytics +) { + + private var cachedValidationStatus: Pair>? = null + + fun present() { + view.setupUI() + handleValuesChange() + handleSubmit() + handleCancel() + } + + private fun handleCancel() { + disposables.add( + view.getCancelClicks() + .doOnNext { + activity?.closeCancel(true) + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleSubmit() { + disposables.add( + view.getSubmitClicks() + .doOnNext { view.setButtonState(false) } + .subscribeOn(viewScheduler) + .flatMapSingle { + smsValidationInteract.requestValidationCode("${it.first}${it.second}") + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { status -> + cachedValidationStatus = Pair(status, it) + view.setButtonState(true) + onSuccess(status, it) + } + .doOnError { view.setButtonState(true) } + .doOnSuccess { cachedValidationStatus = null } + } + .retry() + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun onSuccess(status: WalletValidationStatus, submitInfo: Pair) { + handlePhoneValidationAnalytics("submit", status) + when (status) { + WalletValidationStatus.SUCCESS -> activity?.showCodeValidationView(submitInfo.first, + submitInfo.second) + WalletValidationStatus.TOO_MANY_ATTEMPTS -> showErrorMessage( + R.string.verification_error_attempts_reached) + WalletValidationStatus.INVALID_INPUT, + WalletValidationStatus.INVALID_PHONE -> { + showErrorMessage(R.string.verification_insert_phone_field_number_error) + view.setButtonState(false) + } + WalletValidationStatus.DOUBLE_SPENT -> { + showErrorMessage(R.string.verification_insert_phone_field_phone_used_already_error) + view.setButtonState(false) + } + WalletValidationStatus.NO_NETWORK, + WalletValidationStatus.GENERIC_ERROR -> showErrorMessage(R.string.unknown_error) + WalletValidationStatus.LANDLINE_NOT_SUPPORTED -> { + showErrorMessage(R.string.verification_insert_phone_field_landline_error) + view.setButtonState(false) + } + WalletValidationStatus.REGION_NOT_SUPPORTED -> { + showErrorMessage(R.string.verification_insert_phone_field_region_error) + view.setButtonState(false) + } + } + } + + private fun handlePhoneValidationAnalytics(action: String, status: WalletValidationStatus) { + if (status == WalletValidationStatus.SUCCESS) { + analytics.sendPhoneVerificationEvent(action, "poa", "success", "") + } else { + analytics.sendPhoneVerificationEvent(action, "poa", "error", status.name) + } + } + + private fun showErrorMessage(@StringRes errorMessage: Int) = view.setError(errorMessage) + + private fun handleValuesChange() { + disposables.add( + Observable.combineLatest( + view.getCountryCode(), + view.getPhoneNumber(), + BiFunction { countryCode: String, phoneNumber: String -> + view.clearError() + if (hasValidData(countryCode, phoneNumber)) { + view.setButtonState(true) + } else { + view.setButtonState(false) + } + }) + .subscribe({ }, { throwable -> throwable.printStackTrace() })) + } + + private fun hasValidData(countryCode: String, phoneNumber: String): Boolean { + return phoneNumber.isNotBlank() && countryCode.isNotBlank() + } + + fun stop() = disposables.dispose() + + fun onResume() = resumePreviousState() + + private fun resumePreviousState() { + cachedValidationStatus?.let { onSuccess(it.first, it.second); cachedValidationStatus = null } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/PhoneValidationDialogView.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/PhoneValidationDialogView.kt new file mode 100644 index 00000000000..41c68d69b48 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/PhoneValidationDialogView.kt @@ -0,0 +1,23 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import io.reactivex.Observable + +interface PhoneValidationDialogView { + + fun setupUI() + + fun getCountryCode(): Observable + + fun getPhoneNumber(): Observable + + fun setButtonState(state: Boolean) + + fun getSubmitClicks(): Observable> + + fun getCancelClicks(): Observable + + fun setError(message: Int) + + fun clearError() + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationLoadingDialogFragment.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationLoadingDialogFragment.kt new file mode 100644 index 00000000000..717f2d7e2e6 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationLoadingDialogFragment.kt @@ -0,0 +1,99 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.asfoundation.wallet.wallet_validation.ValidationInfo +import com.asfoundation.wallet.wallet_validation.generic.WalletValidationAnalytics +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.fragment_validation_loading.* +import javax.inject.Inject + +class ValidationLoadingDialogFragment : BasePageViewFragment(), ValidationLoadingDialogView { + + companion object { + @JvmStatic + fun newInstance(validationInfo: ValidationInfo): ValidationLoadingDialogFragment { + val bundle = Bundle().apply { + putSerializable(VALIDATION, validationInfo) + } + return ValidationLoadingDialogFragment().apply { arguments = bundle } + } + + private const val VALIDATION = "validation" + } + + @Inject + lateinit var findDefaultWalletInteract: FindDefaultWalletInteract + + @Inject + lateinit var smsValidationInteract: SmsValidationInteract + + @Inject + lateinit var analytics: WalletValidationAnalytics + + private lateinit var presenter: ValidationLoadingDialogPresenter + + private lateinit var walletValidationDialogView: WalletValidationDialogView + + val data: ValidationInfo by lazy { + if (arguments!!.containsKey(VALIDATION)) { + arguments!!.getSerializable(VALIDATION) as ValidationInfo + } else { + throw IllegalArgumentException("previous validation info not found") + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is WalletValidationDialogView) { + throw IllegalStateException( + "Express checkout buy fragment must be attached to IAB activity") + } + walletValidationDialogView = context + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + presenter = + ValidationLoadingDialogPresenter(this, walletValidationDialogView, + findDefaultWalletInteract, + smsValidationInteract, data, AndroidSchedulers.mainThread(), Schedulers.io(), + CompositeDisposable(), analytics) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_validation_loading, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + presenter.present() + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + override fun show() { + validation_loading_animation.setAnimation(R.raw.transact_loading_animation) + validation_loading_animation.playAnimation() + } + + override fun clean() { + validation_loading_animation.removeAllAnimatorListeners() + validation_loading_animation.removeAllUpdateListeners() + validation_loading_animation.removeAllLottieOnCompositionLoadedListener() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationLoadingDialogPresenter.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationLoadingDialogPresenter.kt new file mode 100644 index 00000000000..bf38ac8cc24 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationLoadingDialogPresenter.kt @@ -0,0 +1,83 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import com.asf.wallet.R +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.wallet_validation.ValidationInfo +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import com.asfoundation.wallet.wallet_validation.generic.WalletValidationAnalytics +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.TimeUnit + +class ValidationLoadingDialogPresenter( + private val view: ValidationLoadingDialogView, + private val activity: WalletValidationDialogView?, + private val defaultWalletInteract: FindDefaultWalletInteract, + private val smsValidationInteract: SmsValidationInteract, + private val validationInfo: ValidationInfo, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val analytics: WalletValidationAnalytics +) { + + fun present() { + view.show() + + handleValidationWallet() + } + + private fun handleValidationWallet() { + disposables.add( + defaultWalletInteract.find() + .delay(1, TimeUnit.SECONDS) + .flatMap { wallet -> + smsValidationInteract.validateCode( + "+${validationInfo.countryCode}${validationInfo.phoneNumber}", + wallet, + "${validationInfo.code1}${validationInfo.code2}${validationInfo.code3}${validationInfo.code4}${validationInfo.code5}${validationInfo.code6}") + } + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .subscribe({ status -> handleNext(status) }, { it.printStackTrace() }) + ) + } + + private fun handleNext(status: WalletValidationStatus) { + handleVerificationAnalytics(status) + when (status) { + WalletValidationStatus.SUCCESS -> activity?.showSuccess() + WalletValidationStatus.INVALID_CODE, + WalletValidationStatus.INVALID_INPUT -> handleError( + R.string.verification_insert_code_error) + WalletValidationStatus.INVALID_PHONE -> + activity?.showPhoneValidationView(validationInfo.countryCode, validationInfo.phoneNumber, + R.string.verification_insert_code_error_common) + WalletValidationStatus.DOUBLE_SPENT -> + activity?.showSuccess() + WalletValidationStatus.TOO_MANY_ATTEMPTS -> handleError( + R.string.verification_error_attempts_reached) + WalletValidationStatus.EXPIRED_CODE -> handleError(R.string.verification_error_time_expired) + WalletValidationStatus.GENERIC_ERROR -> handleError(R.string.unknown_error) + } + } + + private fun handleVerificationAnalytics(status: WalletValidationStatus) { + if (status == WalletValidationStatus.SUCCESS) { + analytics.sendConfirmationEvent("success", "") + } else { + analytics.sendConfirmationEvent("error", status.name) + } + } + + private fun handleError(errorMessage: Int) { + activity?.showCodeValidationView(validationInfo, errorMessage) + } + + fun stop() { + disposables.clear() + view.clean() + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationLoadingDialogView.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationLoadingDialogView.kt new file mode 100644 index 00000000000..2fd2f91ef54 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationLoadingDialogView.kt @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +interface ValidationLoadingDialogView { + + fun show() + + fun clean() + +} diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationSuccessDialogFragment.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationSuccessDialogFragment.kt new file mode 100644 index 00000000000..bfb4a1bdfa4 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationSuccessDialogFragment.kt @@ -0,0 +1,108 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import android.animation.Animator +import android.app.NotificationManager +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.asf.wallet.R +import com.asfoundation.wallet.advertise.WalletPoAService.VERIFICATION_SERVICE_ID +import com.asfoundation.wallet.poa.ProofOfAttentionService +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject +import kotlinx.android.synthetic.main.fragment_validation_success.* +import javax.inject.Inject + +class ValidationSuccessDialogFragment : BasePageViewFragment(), ValidationSuccessDialogView { + + @Inject + lateinit var proofOfAttentionService: ProofOfAttentionService + + private lateinit var walletValidationDialogView: WalletValidationDialogView + private lateinit var presenter: ValidationSuccessDialogPresenter + private lateinit var notificationManager: NotificationManager + + private lateinit var animationCompleted: Subject + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context !is WalletValidationDialogView) { + throw IllegalStateException( + "Validation Success fragment must be attached to Wallet Validation Activity") + } + walletValidationDialogView = context + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + notificationManager = + context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + + animationCompleted = BehaviorSubject.create() + + presenter = + ValidationSuccessDialogPresenter(this, proofOfAttentionService, CompositeDisposable(), + walletValidationDialogView) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_validation_success, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + presenter.present() + } + + override fun onDestroyView() { + presenter.stop() + super.onDestroyView() + } + + override fun setupUI() { + validation_success_animation.setAnimation(R.raw.success_animation) + validation_success_animation.playAnimation() + validation_success_animation.repeatCount = 0 + validation_success_animation.addAnimatorListener(object : Animator.AnimatorListener { + override fun onAnimationRepeat(animation: Animator?) { + } + + override fun onAnimationEnd(animation: Animator?) { + animationCompleted.onNext(true) + notificationManager.cancel(VERIFICATION_SERVICE_ID) + walletValidationDialogView.closeSuccess() + } + + override fun onAnimationCancel(animation: Animator?) { + } + + override fun onAnimationStart(animation: Animator?) { + } + }) + } + + override fun handleAnimationEnd(): Observable { + return animationCompleted + } + + override fun clean() { + validation_success_animation.removeAllAnimatorListeners() + validation_success_animation.removeAllUpdateListeners() + validation_success_animation.removeAllLottieOnCompositionLoadedListener() + } + + companion object { + @JvmStatic + fun newInstance(): ValidationSuccessDialogFragment { + return ValidationSuccessDialogFragment() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationSuccessDialogPresenter.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationSuccessDialogPresenter.kt new file mode 100644 index 00000000000..e87d33d6415 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationSuccessDialogPresenter.kt @@ -0,0 +1,36 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import com.asfoundation.wallet.poa.ProofOfAttentionService +import io.reactivex.Completable +import io.reactivex.disposables.CompositeDisposable + +class ValidationSuccessDialogPresenter( + private val view: ValidationSuccessDialogView, + private val service: ProofOfAttentionService, + private val disposables: CompositeDisposable, + private val activity: WalletValidationDialogView? +) { + + fun present() { + view.setupUI() + handleAnimationEnd() + } + + private fun handleAnimationEnd() { + disposables.add( + view.handleAnimationEnd() + .filter { animationCompleted -> animationCompleted } + .flatMapCompletable { Completable.fromAction { service.setWalletValidated() } } + .subscribe({}, { + it.printStackTrace() + activity?.closeSuccess() + }) + ) + } + + fun stop() { + disposables.clear() + view.clean() + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationSuccessDialogView.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationSuccessDialogView.kt new file mode 100644 index 00000000000..ff1db0f11c7 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/ValidationSuccessDialogView.kt @@ -0,0 +1,13 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import io.reactivex.Observable + +interface ValidationSuccessDialogView { + + fun setupUI() + + fun clean() + + fun handleAnimationEnd(): Observable + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/WalletValidationBroadcastReceiver.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/WalletValidationBroadcastReceiver.kt new file mode 100644 index 00000000000..7cda21fd8ec --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/WalletValidationBroadcastReceiver.kt @@ -0,0 +1,40 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.asfoundation.wallet.advertise.WalletPoAService.VERIFICATION_SERVICE_ID + +class WalletValidationBroadcastReceiver : BroadcastReceiver() { + + private lateinit var notificationManager: NotificationManager + + companion object { + + const val ACTION_KEY = "ACTION_KEY" + const val ACTION_START_VALIDATION = "ACTION_START_VALIDATION" + const val ACTION_DISMISS = "ACTION_DISMISS" + + @JvmStatic + fun newIntent(context: Context): Intent { + return Intent(context, WalletValidationBroadcastReceiver::class.java) + } + } + + override fun onReceive(context: Context, intent: Intent) { + notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + notificationManager.cancel(VERIFICATION_SERVICE_ID) + context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) + + if (intent.getStringExtra(ACTION_KEY) == ACTION_START_VALIDATION) { + val validationIntent = WalletValidationDialogActivity.newIntent(context) + .apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + context.startActivity(validationIntent) + } else if (intent.getStringExtra(ACTION_KEY) == ACTION_DISMISS) return + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/WalletValidationDialogActivity.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/WalletValidationDialogActivity.kt new file mode 100644 index 00000000000..4c2c22fd08f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/WalletValidationDialogActivity.kt @@ -0,0 +1,168 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.PersistableBundle +import android.view.View.GONE +import android.view.View.VISIBLE +import androidx.annotation.StringRes +import com.appcoins.wallet.bdsbilling.WalletService +import com.asf.wallet.R +import com.asfoundation.wallet.repository.SmsValidationRepositoryType +import com.asfoundation.wallet.ui.BaseActivity +import com.asfoundation.wallet.ui.iab.IabActivity.Companion.ERROR_MESSAGE +import com.asfoundation.wallet.wallet_validation.ValidationInfo +import dagger.android.AndroidInjection +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.activity_iab_wallet_creation.* +import javax.inject.Inject + +class WalletValidationDialogActivity : BaseActivity(), + WalletValidationDialogView { + + private lateinit var presenter: WalletValidationDialogPresenter + + @Inject + lateinit var smsValidationRepository: SmsValidationRepositoryType + + @Inject + lateinit var walletService: WalletService + private var walletValidated: Boolean = false + + companion object { + private const val RESULT_OK = 0 + private const val RESULT_CANCELED = 1 + private const val RESULT_FAILED = 2 + private const val WALLET_VALIDATED_KEY = "wallet_validated" + + @JvmStatic + fun newIntent(context: Context): Intent { + return Intent(context, WalletValidationDialogActivity::class.java) + } + + @JvmStatic + fun newIntent(context: Context, @StringRes error: Int): Intent { + return Intent(context, WalletValidationDialogActivity::class.java).apply { + putExtra(ERROR_MESSAGE, error) + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_poa_wallet_validation) + savedInstanceState?.let { + walletValidated = it.getBoolean(WALLET_VALIDATED_KEY, false) + } + presenter = WalletValidationDialogPresenter(this, smsValidationRepository, walletService, + CompositeDisposable(), AndroidSchedulers.mainThread(), + Schedulers.io()) + presenter.present() + } + + override fun onBackPressed() { + if (walletValidated) { + closeSuccess() + } else { + closeCancel(false) + } + super.onBackPressed() + } + + override fun onDestroy() { + presenter.stop() + super.onDestroy() + } + + override fun onSaveInstanceState(outState: Bundle?, outPersistentState: PersistableBundle?) { + super.onSaveInstanceState(outState, outPersistentState) + outState?.putBoolean( + WALLET_VALIDATED_KEY, walletValidated) + } + + override fun showPhoneValidationView(countryCode: String?, phoneNumber: String?, + errorMessage: Int?) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + PhoneValidationDialogFragment.newInstance( + countryCode, phoneNumber, errorMessage)) + .commit() + } + + override fun showCodeValidationView(countryCode: String, phoneNumber: String) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + CodeValidationDialogFragment.newInstance( + countryCode, phoneNumber)) + .commit() + } + + override fun showCodeValidationView(validationInfo: ValidationInfo, errorMessage: Int) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + CodeValidationDialogFragment.newInstance(validationInfo, errorMessage)) + .commit() + } + + override fun showLoading(it: ValidationInfo) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, ValidationLoadingDialogFragment.newInstance(it)) + .commit() + } + + override fun showSuccess() { + walletValidated = true + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, ValidationSuccessDialogFragment.newInstance()) + .commit() + } + + override fun closeSuccess() { + val intent = Intent().apply { + putExtra(ERROR_MESSAGE, errorMessage) + } + setResult(RESULT_OK, intent) + finishAndRemoveTask() + } + + override fun closeCancel(removeTask: Boolean) { + val intent = Intent().apply { + putExtra(ERROR_MESSAGE, errorMessage) + } + setResult(RESULT_CANCELED, intent) + if (removeTask) { + finishAndRemoveTask() + } else { + finish() + } + } + + override fun closeError() { + val intent = Intent().apply { + putExtra(ERROR_MESSAGE, errorMessage) + } + setResult(RESULT_FAILED, intent) + finishAndRemoveTask() + } + + override fun showCreateAnimation() { + create_wallet_card.visibility = VISIBLE + create_wallet_animation.visibility = VISIBLE + create_wallet_animation.playAnimation() + create_wallet_text.visibility = VISIBLE + } + + override fun hideAnimation() { + create_wallet_card.visibility = GONE + create_wallet_animation.visibility = GONE + create_wallet_text.visibility = GONE + } + + private val errorMessage: Int by lazy { + intent.getIntExtra(ERROR_MESSAGE, 0) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/WalletValidationDialogPresenter.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/WalletValidationDialogPresenter.kt new file mode 100644 index 00000000000..76b1dc17a3f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/WalletValidationDialogPresenter.kt @@ -0,0 +1,48 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import com.appcoins.wallet.bdsbilling.WalletService +import com.asfoundation.wallet.repository.SmsValidationRepositoryType +import com.asfoundation.wallet.service.WalletGetterStatus +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus.SUCCESS +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable + +class WalletValidationDialogPresenter( + private val dialogView: WalletValidationDialogView, + private val smsValidationRepository: SmsValidationRepositoryType, + private val accountWalletService: WalletService, + private val disposables: CompositeDisposable, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler +) { + + fun present() { + handleWalletValidation() + } + + private fun handleWalletValidation() { + disposables.add(accountWalletService.findWalletOrCreate() + .doOnNext { + if (it == WalletGetterStatus.CREATING.toString()) { + dialogView.showCreateAnimation() + } + } + .filter { it != WalletGetterStatus.CREATING.toString() } + .flatMap { + smsValidationRepository.isValid(it) + .toObservable() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + } + .map { + if (it == SUCCESS) dialogView.closeSuccess() + else dialogView.showPhoneValidationView(null, null) + } + .subscribe({}, { + it.printStackTrace() + dialogView.closeError() + })) + } + + fun stop() = disposables.clear() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/WalletValidationDialogView.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/WalletValidationDialogView.kt new file mode 100644 index 00000000000..3823b3181c8 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/dialog/WalletValidationDialogView.kt @@ -0,0 +1,26 @@ +package com.asfoundation.wallet.wallet_validation.dialog + +import com.asfoundation.wallet.wallet_validation.ValidationInfo + +interface WalletValidationDialogView { + + fun showPhoneValidationView(countryCode: String?, phoneNumber: String?, errorMessage: Int? = null) + + fun showCodeValidationView(countryCode: String, phoneNumber: String) + + fun showCodeValidationView(validationInfo: ValidationInfo, errorMessage: Int) + + fun showLoading(it: ValidationInfo) + + fun showSuccess() + + fun closeSuccess() + + fun closeCancel(removeTask: Boolean) + + fun closeError() + + fun showCreateAnimation() + + fun hideAnimation() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/Code.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/Code.kt new file mode 100644 index 00000000000..030e5dabb6d --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/Code.kt @@ -0,0 +1,4 @@ +package com.asfoundation.wallet.wallet_validation.generic + +data class Code(val code1: String?, val code2: String?, val code3: String?, val code4: String?, + val code5: String?, val code6: String?) \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/CodeValidationFragment.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/CodeValidationFragment.kt new file mode 100644 index 00000000000..25d3294e0f0 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/CodeValidationFragment.kt @@ -0,0 +1,451 @@ +package com.asfoundation.wallet.wallet_validation.generic + +import android.content.ClipboardManager +import android.content.Context +import android.content.Context.CLIPBOARD_SERVICE +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import androidx.fragment.app.Fragment +import com.asf.wallet.R +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.referrals.ReferralInteractorContract +import com.asfoundation.wallet.viewmodel.BasePageViewFragment +import com.asfoundation.wallet.wallet_validation.DeleteKeyListener +import com.asfoundation.wallet.wallet_validation.PasteTextWatcher +import com.asfoundation.wallet.wallet_validation.ValidationInfo +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxbinding2.widget.RxTextView +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.layout_code_validation.* +import kotlinx.android.synthetic.main.layout_referral_status.* +import kotlinx.android.synthetic.main.layout_validation_no_internet.* +import kotlinx.android.synthetic.main.layout_validation_result.* +import kotlinx.android.synthetic.main.single_sms_input_layout.view.* +import kotlinx.android.synthetic.main.sms_text_input_layout.* +import javax.inject.Inject + + +class CodeValidationFragment : BasePageViewFragment(), + CodeValidationView { + + @Inject + lateinit var referralInteractor: ReferralInteractorContract + + @Inject + lateinit var smsValidationInteract: SmsValidationInteract + + @Inject + lateinit var defaultWalletInteract: FindDefaultWalletInteract + + @Inject + lateinit var analytics: WalletValidationAnalytics + + private var walletValidationView: WalletValidationView? = null + private lateinit var presenter: CodeValidationPresenter + private lateinit var fragmentContainer: ViewGroup + private lateinit var clipboard: ClipboardManager + private var code: Code? = null + + private val hasBeenInvitedFlow: Boolean by lazy { + arguments!!.getBoolean(HAS_BEEN_INVITED_FLOW) + } + + val countryCode: String by lazy { + try { + arguments!!.getString(PhoneValidationFragment.COUNTRY_CODE)!! + } catch (e: Exception) { + throw IllegalArgumentException("Unable to get Country Code") + } + } + + val phoneNumber: String by lazy { + try { + arguments!!.getString(PhoneValidationFragment.PHONE_NUMBER)!! + } catch (e: Exception) { + throw IllegalArgumentException("Unable to get Phone Number") + } + } + + private val errorMessage: Int? by lazy { + if (arguments!!.containsKey(ERROR_MESSAGE)) { + arguments!!.getInt(ERROR_MESSAGE) + } else { + null + } + } + + private val validationInfo: ValidationInfo? by lazy { + if (arguments!!.containsKey(VALIDATION_INFO)) { + arguments!!.getSerializable(VALIDATION_INFO) as ValidationInfo + } else { + null + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + outState.putString(CODE_1, code_1.code.text.toString()) + outState.putString(CODE_2, code_2.code.text.toString()) + outState.putString(CODE_3, code_3.code.text.toString()) + outState.putString(CODE_4, code_4.code.text.toString()) + outState.putString(CODE_5, code_5.code.text.toString()) + outState.putString(CODE_6, code_6.code.text.toString()) + outState.putBoolean(IS_LAST_STEP, presenter.isLastStep) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + clipboard = context!!.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + + presenter = + CodeValidationPresenter(this, walletValidationView, referralInteractor, + smsValidationInteract, defaultWalletInteract, AndroidSchedulers.mainThread(), + Schedulers.io(), countryCode, phoneNumber, CompositeDisposable(), hasBeenInvitedFlow, + analytics) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + fragmentContainer = container!! + return inflater.inflate(R.layout.layout_code_validation, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupBodyText() + + var lastStep = false + savedInstanceState?.let { + lastStep = it.getBoolean(IS_LAST_STEP) + code = Code(it.getString(CODE_1), it.getString(CODE_2), it.getString(CODE_3), + it.getString(CODE_4), it.getString(CODE_5), it.getString(CODE_6)) + } + + presenter.present(lastStep) + } + + override fun onResume() { + super.onResume() + presenter.onResume(code) + } + + override fun setupUI() { + hideNoInternetView() + if (errorMessage == null) { + error.visibility = View.INVISIBLE + setButtonState(true) + } else { + error.visibility = View.VISIBLE + error.text = getString(errorMessage!!) + setButtonState(false) + } + + validationInfo?.let { + code_1.code.setText(it.code1) + code_2.code.setText(it.code2) + code_3.code.setText(it.code3) + code_4.code.setText(it.code4) + code_5.code.setText(it.code5) + code_6.code.setText(it.code6) + } + + val inputTexts = + arrayOf(code_1.code, code_2.code, code_3.code, code_4.code, code_5.code, code_6.code) + + code_1.code.addTextChangedListener(PasteTextWatcher(inputTexts, clipboard, 0)) + code_2.code.addTextChangedListener(PasteTextWatcher(inputTexts, clipboard, 1)) + code_3.code.addTextChangedListener(PasteTextWatcher(inputTexts, clipboard, 2)) + code_4.code.addTextChangedListener(PasteTextWatcher(inputTexts, clipboard, 3)) + code_5.code.addTextChangedListener(PasteTextWatcher(inputTexts, clipboard, 4)) + code_6.code.addTextChangedListener(PasteTextWatcher(inputTexts, clipboard, 5)) + code_1.code.setOnKeyListener(DeleteKeyListener(inputTexts, 0)) + code_2.code.setOnKeyListener(DeleteKeyListener(inputTexts, 1)) + code_3.code.setOnKeyListener(DeleteKeyListener(inputTexts, 2)) + code_4.code.setOnKeyListener(DeleteKeyListener(inputTexts, 3)) + code_5.code.setOnKeyListener(DeleteKeyListener(inputTexts, 4)) + code_6.code.setOnKeyListener(DeleteKeyListener(inputTexts, 5)) + } + + override fun clearUI() { + error.visibility = View.INVISIBLE + code_1.code.text = null + code_2.code.text = null + code_3.code.text = null + code_4.code.text = null + code_5.code.text = null + code_6.code.text = null + code_1.requestFocus() + } + + override fun setButtonState(state: Boolean) { + submit_button.isEnabled = state + } + + override fun getBackClicks() = RxView.clicks(back_button) + + override fun getRetryButtonClicks(): Observable { + return RxView.clicks(retry_button) + .map { + ValidationInfo(code_1.code.text.toString(), + code_2.code.text.toString(), code_3.code.text.toString(), + code_4.code.text.toString(), code_5.code.text.toString(), + code_6.code.text.toString(), countryCode, + phoneNumber) + } + } + + override fun getLaterButtonClicks() = RxView.clicks(later_button) + + override fun getSubmitClicks(): Observable { + return RxView.clicks(submit_button) + .map { + ValidationInfo(code_1.code.text.toString(), + code_2.code.text.toString(), code_3.code.text.toString(), + code_4.code.text.toString(), code_5.code.text.toString(), + code_6.code.text.toString(), countryCode, + phoneNumber) + } + } + + override fun getOkClicks() = RxView.clicks(ok_button) + + override fun getResentCodeClicks() = RxView.clicks(resend_code) + + override fun getFirstChar(): Observable { + return RxTextView.afterTextChangeEvents(code_1.code) + .filter { it.editable() != null } + .map { + it.editable() + .toString() + } + } + + override fun getSecondChar(): Observable { + return RxTextView.afterTextChangeEvents(code_2.code) + .filter { it.editable() != null } + .map { + it.editable() + .toString() + } + } + + override fun getThirdChar(): Observable { + return RxTextView.afterTextChangeEvents(code_3.code) + .filter { it.editable() != null } + .map { + it.editable() + .toString() + } + } + + override fun getFourthChar(): Observable { + return RxTextView.afterTextChangeEvents(code_4.code) + .filter { it.editable() != null } + .map { + it.editable() + .toString() + } + } + + override fun getFifthChar(): Observable { + return RxTextView.afterTextChangeEvents(code_5.code) + .filter { it.editable() != null } + .map { + it.editable() + ?.toString() + } + } + + override fun getSixthChar(): Observable { + return RxTextView.afterTextChangeEvents(code_6.code) + .filter { it.editable() != null } + .map { + it.editable() + .toString() + } + } + + override fun moveToNextView(current: Int) { + when (current) { + 1 -> code_2.requestFocus() + 2 -> code_3.requestFocus() + 3 -> code_4.requestFocus() + 4 -> code_5.requestFocus() + 5 -> code_6.requestFocus() + } + } + + override fun showLoading() { + content.visibility = View.GONE + referral_status.visibility = View.GONE + animation_validating_code.visibility = View.VISIBLE + validate_code_animation.playAnimation() + } + + override fun showReferralEligible(currency: String, maxAmount: String, minAmount: String) { + walletValidationView?.showLastStepAnimation() + content.visibility = View.GONE + animation_validating_code.visibility = View.GONE + referral_status.visibility = View.VISIBLE + referral_status_title.setText(R.string.referral_verification_confirmation_title) + referral_status_body.text = + getString(R.string.referral_verification_confirmation_body, + currency + maxAmount, currency + minAmount) + referral_status_animation.setAnimation(R.raw.referral_invited) + referral_status_animation.playAnimation() + } + + override fun showReferralIneligible(currency: String, maxAmount: String) { + walletValidationView?.showLastStepAnimation() + content.visibility = View.GONE + animation_validating_code.visibility = View.GONE + referral_status.visibility = View.VISIBLE + referral_status_title.setText(R.string.referral_verification_not_invited_title) + referral_status_body.text = + getString(R.string.referral_verification_not_invited_body, currency + maxAmount) + referral_status_animation.setAnimation(R.raw.referral_not_invited) + referral_status_animation.playAnimation() + } + + override fun showGenericValidationComplete() { + walletValidationView?.showLastStepAnimation() + content.visibility = View.GONE + animation_validating_code.visibility = View.GONE + referral_status.visibility = View.VISIBLE + referral_status_title.setText(R.string.verification_completed_title) + referral_status_body.setText(R.string.verification_completed_body) + referral_status_animation.setAnimation(R.raw.referral_invited) + referral_status_animation.playAnimation() + } + + override fun setCode(code: Code) { + code.code1?.let { code_1.code.setText(it) } + code.code2?.let { code_2.code.setText(it) } + code.code3?.let { code_3.code.setText(it) } + code.code4?.let { code_4.code.setText(it) } + code.code5?.let { code_5.code.setText(it) } + code.code6?.let { code_6.code.setText(it) } + this.code = null + } + + override fun showNoInternetView() { + walletValidationView?.hideProgressAnimation() + stopRetryAnimation() + content.visibility = View.GONE + referral_status.visibility = View.GONE + animation_validating_code.visibility = View.GONE + code_layout_validation_no_internet.visibility = View.VISIBLE + } + + override fun hideNoInternetView() { + walletValidationView?.showProgressAnimation() + code_layout_validation_no_internet.visibility = View.GONE + } + + private fun stopRetryAnimation() { + retry_button.visibility = View.VISIBLE + later_button.visibility = View.VISIBLE + retry_animation.visibility = View.GONE + } + + override fun onDestroy() { + presenter.stop() + super.onDestroy() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + + require( + context is WalletValidationView) { CodeValidationFragment::class.java.simpleName + " needs to be attached to a " + WalletValidationView::class.java.simpleName } + + walletValidationView = context + } + + override fun onDetach() { + super.onDetach() + walletValidationView = null + } + + override fun hideKeyboard() { + val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.hideSoftInputFromWindow(fragmentContainer.windowToken, 0) + code_6.clearFocus() + } + + companion object { + + internal const val ERROR_MESSAGE = "ERROR_MESSAGE" + internal const val VALIDATION_INFO = "VALIDATION_INFO" + internal const val HAS_BEEN_INVITED_FLOW = "HAS_BEEN_INVITED_FLOW" + internal const val CODE_1 = "code1" + internal const val CODE_2 = "code2" + internal const val CODE_3 = "code3" + internal const val CODE_4 = "code4" + internal const val CODE_5 = "code5" + internal const val CODE_6 = "code6" + internal const val IS_LAST_STEP = "lastStep" + + @JvmStatic + fun newInstance(countryCode: String, phoneNumber: String, + hasBeenInvitedFlow: Boolean = true): Fragment { + val bundle = Bundle().apply { + putString(PhoneValidationFragment.COUNTRY_CODE, countryCode) + putString(PhoneValidationFragment.PHONE_NUMBER, phoneNumber) + putBoolean(HAS_BEEN_INVITED_FLOW, hasBeenInvitedFlow) + } + + return CodeValidationFragment().apply { arguments = bundle } + } + + @JvmStatic + fun newInstance(info: ValidationInfo, errorMessage: Int, + hasBeenInvitedFlow: Boolean = true): Fragment { + val bundle = Bundle().apply { + putString(PhoneValidationFragment.COUNTRY_CODE, info.countryCode) + putString(PhoneValidationFragment.PHONE_NUMBER, info.phoneNumber) + putInt(ERROR_MESSAGE, errorMessage) + putSerializable(VALIDATION_INFO, info) + putBoolean(HAS_BEEN_INVITED_FLOW, hasBeenInvitedFlow) + } + + return CodeValidationFragment().apply { arguments = bundle } + } + } + + override fun focusAndShowKeyboard() { + val view = getViewToFocus() + view?.post { + view.requestFocus() + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.showSoftInput(view, InputMethodManager.SHOW_FORCED) + } + } + + private fun getViewToFocus(): View? { + return when { + code_1.code.text.isBlank() -> code_1.code + code_2.code.text.isBlank() -> code_2.code + code_3.code.text.isBlank() -> code_3.code + code_4.code.text.isBlank() -> code_4.code + code_5.code.text.isBlank() -> code_5.code + code_6.code.text.isBlank() -> code_6.code + else -> null + } + } + + private fun setupBodyText() { + if (!hasBeenInvitedFlow) { + code_validation_subtitle.text = getString(R.string.verification_insert_phone_body) + } + } +} diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/CodeValidationPresenter.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/CodeValidationPresenter.kt new file mode 100644 index 00000000000..728c45aefee --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/CodeValidationPresenter.kt @@ -0,0 +1,267 @@ +package com.asfoundation.wallet.wallet_validation.generic + +import com.asf.wallet.R +import com.asfoundation.wallet.interact.FindDefaultWalletInteract +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.referrals.ReferralInteractorContract +import com.asfoundation.wallet.util.scaleToString +import com.asfoundation.wallet.wallet_validation.ValidationInfo +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.Function6 +import java.math.BigDecimal +import java.util.concurrent.TimeUnit + +class CodeValidationPresenter( + private val view: CodeValidationView, + private val activity: WalletValidationView?, + private val referralInteractor: ReferralInteractorContract, + private val smsValidationInteract: SmsValidationInteract, + private val defaultWalletInteract: FindDefaultWalletInteract, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val countryCode: String, + private val phoneNumber: String, + private val disposables: CompositeDisposable, + private val hasBeenInvitedFlow: Boolean, + private val analytics: WalletValidationAnalytics +) { + + var isLastStep = false + + fun present(lastStep: Boolean) { + view.setupUI() + handleBack() + handleCode() + handleResendCode() + handleValuesChange() + handleSubmitAndRetryClicks() + handleOkClicks() + handleLaterClicks() + + isLastStep = lastStep + if (lastStep) { + checkReferralAvailability() + } + } + + private fun handleLaterClicks() { + disposables.add( + view.getLaterButtonClicks() + .doOnNext { + analytics.sendCodeVerificationEvent("later") + activity?.finishCancelActivity() + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleResendCode() { + disposables.add( + view.getResentCodeClicks() + .doOnNext { + view.clearUI() + } + .subscribeOn(viewScheduler) + .flatMapSingle { + smsValidationInteract.requestValidationCode("+$countryCode$phoneNumber") + .subscribeOn(networkScheduler) + } + .retry() + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleSubmitAndRetryClicks() { + disposables.add( + Observable.merge(view.getSubmitClicks(), view.getRetryButtonClicks()) + .doOnEach { view.hideNoInternetView() } + .doOnEach { view.showLoading() } + .doOnEach { analytics.sendCodeVerificationEvent("next") } + .flatMapSingle { validationInfo -> + defaultWalletInteract.find() + .delay(2, TimeUnit.SECONDS) + .flatMap { wallet -> + smsValidationInteract.validateCode( + "+${validationInfo.countryCode}${validationInfo.phoneNumber}", + wallet, + "${validationInfo.code1}${validationInfo.code2}${validationInfo.code3}${validationInfo.code4}${validationInfo.code5}${validationInfo.code6}") + .observeOn(viewScheduler) + .doOnSuccess { handleNext(it, validationInfo) } + } + .subscribeOn(networkScheduler) + } + .retry() + .observeOn(viewScheduler) + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleBack() { + disposables.add( + view.getBackClicks() + .doOnNext { + analytics.sendCodeVerificationEvent("back") + activity?.showPhoneValidationView(countryCode, phoneNumber) + } + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun handleOkClicks() { + disposables.add( + view.getOkClicks() + .doOnNext { activity?.finishSuccessActivity() } + .subscribe() + ) + } + + private fun checkReferralAvailability() { + if (!hasBeenInvitedFlow) { + view.showGenericValidationComplete() + isLastStep = true + } else { + disposables.add(referralInteractor.retrieveReferral() + .subscribeOn(networkScheduler) + .observeOn(viewScheduler) + .doOnSuccess { + handleReferralStatus(it.invited, it.symbol, it.maxAmount, it.pendingAmount, + it.minAmount) + isLastStep = true + } + .subscribe() + ) + } + } + + private fun handleReferralStatus(eligible: Boolean, currency: String, maxAmount: BigDecimal, + pendingAmount: BigDecimal, minAmount: BigDecimal) { + if (eligible) { + view.showReferralEligible(currency, pendingAmount.scaleToString(2), + minAmount.scaleToString(2)) + } else { + view.showReferralIneligible(currency, maxAmount.scaleToString(2)) + } + } + + private fun handleNext(status: WalletValidationStatus, + validationInfo: ValidationInfo) { + handleVerificationAnalytics(status) + when (status) { + WalletValidationStatus.SUCCESS -> checkReferralAvailability() + WalletValidationStatus.INVALID_CODE, + WalletValidationStatus.INVALID_INPUT -> handleError( + R.string.verification_insert_code_error, validationInfo) + WalletValidationStatus.INVALID_PHONE -> + activity?.showPhoneValidationView(validationInfo.countryCode, validationInfo.phoneNumber, + R.string.verification_insert_code_error_common) + WalletValidationStatus.DOUBLE_SPENT -> checkReferralAvailability() + WalletValidationStatus.TOO_MANY_ATTEMPTS -> handleError( + R.string.verification_error_attempts_reached, validationInfo) + WalletValidationStatus.EXPIRED_CODE -> handleError(R.string.verification_error_time_expired, + validationInfo) + WalletValidationStatus.GENERIC_ERROR -> handleError(R.string.unknown_error, validationInfo) + WalletValidationStatus.NO_NETWORK -> { + view.hideKeyboard() + view.showNoInternetView() + } + } + } + + private fun handleVerificationAnalytics(status: WalletValidationStatus) { + if (status == WalletValidationStatus.SUCCESS) { + analytics.sendConfirmationEvent("success", "") + } else { + analytics.sendConfirmationEvent("error", status.name) + } + } + + private fun handleError(errorMessage: Int, validationInfo: ValidationInfo) { + activity?.showCodeValidationView(validationInfo, errorMessage) + } + + private fun handleValuesChange() { + disposables.add( + Observable.combineLatest( + view.getFirstChar(), + view.getSecondChar(), + view.getThirdChar(), + view.getFourthChar(), + view.getFifthChar(), + view.getSixthChar(), + Function6 { first: String, second: String, third: String, fourth: String, fifth: String, sixth: String -> + if (isValidInput(first, second, third, fourth, fifth, sixth)) { + view.setButtonState(true) + } else { + view.setButtonState(false) + } + }) + .subscribe({}, { it.printStackTrace() })) + } + + private fun isValidInput(first: String, second: String, third: String, fourth: String, + fifth: String, sixth: String): Boolean { + return first.isNotBlank() && + second.isNotBlank() && + third.isNotBlank() && + fourth.isNotBlank() && + fifth.isNotBlank() && + sixth.isNotBlank() + } + + private fun handleCode() { + disposables.add(view.getFirstChar() + .filter { it.isNotBlank() } + .doOnNext { + view.moveToNextView(1) + } + .subscribe({}, { it.printStackTrace() })) + + disposables.add(view.getSecondChar() + .filter { it.isNotBlank() } + .doOnNext { + view.moveToNextView(2) + } + .subscribe({}, { it.printStackTrace() })) + + disposables.add(view.getThirdChar() + .filter { it.isNotBlank() } + .doOnNext { + view.moveToNextView(3) + } + .subscribe({}, { it.printStackTrace() })) + + disposables.add(view.getFourthChar() + .filter { it.isNotBlank() } + .doOnNext { + view.moveToNextView(4) + } + .subscribe({}, { it.printStackTrace() })) + + disposables.add(view.getFifthChar() + .filter { it.isNotBlank() } + .doOnNext { + view.moveToNextView(5) + } + .subscribe({}, { it.printStackTrace() })) + + disposables.add(view.getSixthChar() + .filter { it.isNotBlank() } + .doOnNext { + view.hideKeyboard() + } + .subscribe({}, { it.printStackTrace() })) + } + + fun stop() { + disposables.dispose() + } + + fun onResume(code: Code?) { + if (!isLastStep) { + code?.let { view.setCode(code) } + view.focusAndShowKeyboard() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/CodeValidationView.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/CodeValidationView.kt new file mode 100644 index 00000000000..2e5f0ab12c6 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/CodeValidationView.kt @@ -0,0 +1,57 @@ +package com.asfoundation.wallet.wallet_validation.generic + +import com.asfoundation.wallet.wallet_validation.ValidationInfo +import io.reactivex.Observable + +interface CodeValidationView { + + fun setupUI() + + fun clearUI() + + fun getBackClicks(): Observable + + fun getSubmitClicks(): Observable + + fun getResentCodeClicks(): Observable + + fun getFirstChar(): Observable + + fun getSecondChar(): Observable + + fun getThirdChar(): Observable + + fun getFourthChar(): Observable + + fun getFifthChar(): Observable + + fun getSixthChar(): Observable + + fun moveToNextView(current: Int) + + fun setButtonState(state: Boolean) + + fun hideKeyboard() + + fun showLoading() + + fun showReferralEligible(currency: String, maxAmount: String, minAmount: String) + + fun showReferralIneligible(currency: String, maxAmount: String) + + fun getOkClicks(): Observable + + fun showNoInternetView() + + fun hideNoInternetView() + + fun getRetryButtonClicks(): Observable + + fun getLaterButtonClicks(): Observable + + fun showGenericValidationComplete() + + fun setCode(code: Code) + + fun focusAndShowKeyboard() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/PhoneValidationFragment.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/PhoneValidationFragment.kt new file mode 100644 index 00000000000..28b6089f70b --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/PhoneValidationFragment.kt @@ -0,0 +1,292 @@ +package com.asfoundation.wallet.wallet_validation.generic + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import androidx.fragment.app.Fragment +import com.asf.wallet.R +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.logging.Logger +import com.hbb20.CountryCodePicker +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxbinding2.widget.RxTextView +import dagger.android.support.DaggerFragment +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.layout_phone_validation.* +import kotlinx.android.synthetic.main.layout_validation_no_internet.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + + +class PhoneValidationFragment : DaggerFragment(), + PhoneValidationView { + + @Inject + lateinit var interactor: SmsValidationInteract + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var analytics: WalletValidationAnalytics + private var walletValidationView: WalletValidationView? = null + private lateinit var presenter: PhoneValidationPresenter + private lateinit var fragmentContainer: ViewGroup + + private var countryCode: String? = null + private var phoneNumber: String? = null + private var errorMessage: Int? = null + private var previousContext: String = "" + + private val hasBeenInvitedFlow: Boolean by lazy { + arguments!!.getBoolean(HAS_BEEN_INVITED_FLOW) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + presenter = PhoneValidationPresenter(this, walletValidationView, interactor, logger, + AndroidSchedulers.mainThread(), Schedulers.io(), CompositeDisposable(), analytics) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + fragmentContainer = container!! + return inflater.inflate(R.layout.layout_phone_validation, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (arguments?.containsKey(COUNTRY_CODE) == true) { + countryCode = arguments?.getString(COUNTRY_CODE) + } + if (arguments?.containsKey(PHONE_NUMBER) == true) { + phoneNumber = arguments?.getString(PHONE_NUMBER) + } + if (arguments?.containsKey(ERROR_MESSAGE) == true) { + errorMessage = arguments?.getInt(ERROR_MESSAGE) + } + if (arguments?.containsKey(PREVIOUS_CONTEXT) == true) { + previousContext = arguments?.getString(PREVIOUS_CONTEXT, "") ?: "" + } + + handleOnSavedInstance(savedInstanceState) + + setupBodyText() + presenter.present() + } + + private fun handleOnSavedInstance(savedInstanceState: Bundle?) { + if (savedInstanceState != null) { + if (savedInstanceState.containsKey(COUNTRY_CODE)) { + countryCode = savedInstanceState.getString(COUNTRY_CODE) + } + if (savedInstanceState.containsKey(ERROR_MESSAGE)) { + errorMessage = savedInstanceState.getInt(ERROR_MESSAGE) + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + outState.putString(COUNTRY_CODE, country_code_picker.selectedCountryCode) + errorMessage?.let { outState.putInt(ERROR_MESSAGE, it) } + } + + override fun onResume() { + super.onResume() + + presenter.onResume(errorMessage) + focusAndShowKeyboard(phone_number) + } + + override fun hideKeyboard() { + val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.hideSoftInputFromWindow(fragmentContainer.windowToken, 0) + content_main.requestFocus() + } + + override fun showNoInternetView() { + walletValidationView?.hideProgressAnimation() + stopRetryAnimation() + content_main.visibility = View.GONE + phone_layout_validation_no_internet.visibility = View.VISIBLE + if (!hasBeenInvitedFlow) later_button.visibility = View.GONE + } + + override fun hideNoInternetView() { + walletValidationView?.showProgressAnimation() + content_main.visibility = View.VISIBLE + phone_layout_validation_no_internet.visibility = View.GONE + } + + override fun getRetryButtonClicks(): Observable { + return RxView.clicks(retry_button) + .map { + PhoneValidationClickData(country_code_picker.selectedCountryCodeWithPlus, + country_code_picker.fullNumber.substringAfter( + country_code_picker.selectedCountryCode), previousContext) + } + .doOnNext { playRetryAnimation() } + .delay(1, TimeUnit.SECONDS) + } + + override fun getLaterButtonClicks(): Observable { + return RxView.clicks(later_button) + .map { PhoneValidationClickData("", "", previousContext) } + } + + override fun setupUI() { + country_code_picker.registerCarrierNumberEditText(phone_number) + country_code_picker.setCustomDialogTextProvider(object : + CountryCodePicker.CustomDialogTextProvider { + override fun getCCPDialogSearchHintText(language: CountryCodePicker.Language?, + defaultSearchHintText: String?) = + defaultSearchHintText ?: "" + + override fun getCCPDialogTitle(language: CountryCodePicker.Language?, defaultTitle: String?) = + getString(R.string.verification_insert_phone_field_country) + + override fun getCCPDialogNoResultACK(language: CountryCodePicker.Language?, + defaultNoResultACK: String?) = defaultNoResultACK ?: "" + }) + + hideNoInternetView() + + countryCode?.let { + country_code_picker.setCountryForPhoneCode(it.drop(0) + .toInt()) + } + phoneNumber?.let { phone_number.setText(it) } + + errorMessage?.let { setError(it) } + } + + override fun setError(message: Int) { + phone_number_layout.error = getString(message) + errorMessage = message + hideNoInternetView() + } + + override fun clearError() { + phone_number_layout.error = null + // This check is needed because this method is always called when restoring the view state and we only want to clear the error when it is the user triggering the changes. + if (isResumed) { + errorMessage = null + } + } + + override fun getCountryCode(): Observable { + return Observable.just(country_code_picker.selectedCountryCodeWithPlus) + } + + override fun getPhoneNumber(): Observable { + return RxTextView.afterTextChangeEvents(phone_number) + .map { + it.editable() + ?.toString() + } + } + + override fun setButtonState(state: Boolean) { + next_button.isEnabled = state + } + + override fun getNextClicks(): Observable { + return RxView.clicks(next_button) + .map { + PhoneValidationClickData(country_code_picker.selectedCountryCodeWithPlus, + country_code_picker.fullNumber.substringAfter( + country_code_picker.selectedCountryCode), previousContext) + } + } + + override fun getCancelClicks(): Observable { + return RxView.clicks(cancel_button) + .map { PhoneValidationClickData("", "", previousContext) } + } + + override fun onDestroy() { + presenter.stop() + super.onDestroy() + } + + private fun stopRetryAnimation() { + retry_button.visibility = View.VISIBLE + if (!hasBeenInvitedFlow) later_button.visibility = View.VISIBLE + retry_animation.visibility = View.GONE + } + + private fun playRetryAnimation() { + retry_button.visibility = View.GONE + later_button.visibility = View.GONE + retry_animation.visibility = View.VISIBLE + retry_animation.playAnimation() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + + require( + context is WalletValidationView) { PhoneValidationFragment::class.java.simpleName + " needs to be attached to a " + WalletValidationView::class.java.simpleName } + + walletValidationView = context + } + + override fun onDetach() { + super.onDetach() + walletValidationView = null + } + + companion object { + + internal const val COUNTRY_CODE = "COUNTRY_CODE" + internal const val PHONE_NUMBER = "PHONE_NUMBER" + internal const val ERROR_MESSAGE = "ERROR_MESSAGE" + internal const val HAS_BEEN_INVITED_FLOW = "HAS_BEEN_INVITED_FLOW" + + private const val PREVIOUS_CONTEXT = "PREVIOUS_CONTEXT" + + @JvmStatic + fun newInstance(countryCode: String? = null, phoneNumber: String? = null, + errorMessage: Int? = null, hasBeenInvitedFlow: Boolean = true, + previousContext: String? = ""): Fragment { + val bundle = Bundle().apply { + putString(COUNTRY_CODE, countryCode) + putString(PHONE_NUMBER, phoneNumber) + putBoolean(HAS_BEEN_INVITED_FLOW, hasBeenInvitedFlow) + putString(PREVIOUS_CONTEXT, previousContext) + + errorMessage?.let { putInt(ERROR_MESSAGE, errorMessage) } + } + + return PhoneValidationFragment().apply { arguments = bundle } + } + + data class PhoneValidationClickData(val countryCode: String, val number: String, + val previousContext: String) + } + + private fun focusAndShowKeyboard(view: EditText) { + view.post { + view.requestFocus() + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.showSoftInput(view, InputMethodManager.SHOW_FORCED) + } + } + + private fun setupBodyText() { + if (!hasBeenInvitedFlow) { + phone_validation_subtitle.text = getString(R.string.verification_insert_phone_body) + } + } + +} diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/PhoneValidationPresenter.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/PhoneValidationPresenter.kt new file mode 100644 index 00000000000..8be21519f62 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/PhoneValidationPresenter.kt @@ -0,0 +1,160 @@ +package com.asfoundation.wallet.wallet_validation.generic + +import androidx.annotation.StringRes +import com.asf.wallet.R +import com.asfoundation.wallet.interact.SmsValidationInteract +import com.asfoundation.wallet.logging.Logger +import com.asfoundation.wallet.wallet_validation.WalletValidationStatus +import com.asfoundation.wallet.wallet_validation.generic.PhoneValidationFragment.Companion.PhoneValidationClickData +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.BiFunction + +class PhoneValidationPresenter( + private val view: PhoneValidationView, + private val activity: WalletValidationView?, + private val smsValidationInteract: SmsValidationInteract, + private val logger: Logger, + private val viewScheduler: Scheduler, + private val networkScheduler: Scheduler, + private val disposables: CompositeDisposable, + private val analytics: WalletValidationAnalytics +) { + companion object { + private val TAG = PhoneValidationPresenter::class.java.simpleName + } + + private var cachedValidationStatus: Pair? = + null + + fun onResume(errorMessage: Int?) { + resumePreviousState(errorMessage) + } + + fun present() { + view.setupUI() + handleValuesChange() + handleNextAndRetryClicks() + handleCancelAndLaterClicks() + } + + private fun resumePreviousState(errorMessage: Int?) { + cachedValidationStatus?.let { onSuccess(it.first, it.second); cachedValidationStatus = null } + errorMessage?.let { + view.setError(it) + view.setButtonState(false) + } + } + + private fun handleCancelAndLaterClicks() { + disposables.add( + Observable.merge(view.getCancelClicks(), view.getLaterButtonClicks()) + .doOnNext { + handlePhoneValidationAnalytics("close", WalletValidationStatus.SUCCESS, + it.previousContext) + view.hideKeyboard() + activity?.finishCancelActivity() + } + .subscribe({}, { it.printStackTrace() })) + } + + private fun handleNextAndRetryClicks() { + disposables.add( + Observable.merge(view.getNextClicks(), view.getRetryButtonClicks()) + .observeOn(viewScheduler) + .doOnNext { view.setButtonState(false) } + .observeOn(networkScheduler) + .flatMapSingle { + smsValidationInteract.requestValidationCode("${it.countryCode}${it.number}") + .observeOn(viewScheduler) + .doOnSuccess { status -> + cachedValidationStatus = Pair(status, it) + view.setButtonState(true) + onSuccess(status, it) + cachedValidationStatus = null + } + .doOnError { throwable -> + analytics.sendPhoneVerificationEvent("submit", it.previousContext, "error", + "generic_error") + view.setButtonState(false) + showErrorMessage(R.string.unknown_error) + logger.log(TAG, throwable.message, throwable) + } + } + .retry() + .subscribe({}, { it.printStackTrace() }) + ) + } + + private fun onSuccess(status: WalletValidationStatus, submitInfo: PhoneValidationClickData) { + handlePhoneValidationAnalytics("submit", status, submitInfo.previousContext) + when (status) { + WalletValidationStatus.SUCCESS -> activity?.showCodeValidationView(submitInfo.countryCode, + submitInfo.number) ?: run { + showErrorMessage(R.string.unknown_error) + logError() + } + WalletValidationStatus.TOO_MANY_ATTEMPTS -> showErrorMessage( + R.string.verification_error_attempts_reached) + WalletValidationStatus.INVALID_INPUT, + WalletValidationStatus.INVALID_PHONE -> { + showErrorMessage(R.string.verification_insert_phone_field_number_error) + view.setButtonState(false) + } + WalletValidationStatus.DOUBLE_SPENT -> { + showErrorMessage(R.string.verification_insert_phone_field_phone_used_already_error) + view.setButtonState(false) + } + WalletValidationStatus.GENERIC_ERROR -> showErrorMessage(R.string.unknown_error) + WalletValidationStatus.NO_NETWORK -> { + view.hideKeyboard() + view.showNoInternetView() + } + WalletValidationStatus.LANDLINE_NOT_SUPPORTED -> { + showErrorMessage(R.string.verification_insert_phone_field_landline_error) + view.setButtonState(false) + } + WalletValidationStatus.REGION_NOT_SUPPORTED -> { + showErrorMessage(R.string.verification_insert_phone_field_region_error) + view.setButtonState(false) + } + } + } + + private fun handlePhoneValidationAnalytics(action: String, status: WalletValidationStatus, + previousContext: String) { + if (status == WalletValidationStatus.SUCCESS) { + analytics.sendPhoneVerificationEvent(action, previousContext, "success", "") + } else { + analytics.sendPhoneVerificationEvent(action, previousContext, "error", status.name) + } + } + + private fun logError() = logger.log(TAG, "Validation Error: Activity null") + + private fun showErrorMessage(@StringRes errorMessage: Int) = view.setError(errorMessage) + + private fun handleValuesChange() { + disposables.add( + Observable.combineLatest( + view.getCountryCode(), + view.getPhoneNumber(), + BiFunction { countryCode: String, phoneNumber: String -> + view.clearError() + if (hasValidData(countryCode, phoneNumber)) { + view.setButtonState(true) + } else { + view.setButtonState(false) + } + }) + .subscribe({ }, { throwable -> throwable.printStackTrace() })) + } + + private fun hasValidData(countryCode: String, phoneNumber: String): Boolean { + return phoneNumber.isNotBlank() && countryCode.isNotBlank() + } + + fun stop() = disposables.dispose() + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/PhoneValidationView.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/PhoneValidationView.kt new file mode 100644 index 00000000000..886f35da411 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/PhoneValidationView.kt @@ -0,0 +1,33 @@ +package com.asfoundation.wallet.wallet_validation.generic + +import io.reactivex.Observable + +interface PhoneValidationView { + + fun setupUI() + + fun getCountryCode(): Observable + + fun getPhoneNumber(): Observable + + fun setButtonState(state: Boolean) + + fun getNextClicks(): Observable + + fun getCancelClicks(): Observable + + fun setError(message: Int) + + fun clearError() + + fun showNoInternetView() + + fun hideNoInternetView() + + fun getRetryButtonClicks(): Observable + + fun getLaterButtonClicks(): Observable + + fun hideKeyboard() + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/WalletValidationActivity.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/WalletValidationActivity.kt new file mode 100644 index 00000000000..ebf6e7616be --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/WalletValidationActivity.kt @@ -0,0 +1,265 @@ +package com.asfoundation.wallet.wallet_validation.generic + +import android.animation.Animator +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.view.View +import androidx.annotation.StringRes +import com.asf.wallet.R +import com.asfoundation.wallet.topup.TopUpActivity.Companion.ERROR_MESSAGE +import com.asfoundation.wallet.ui.BaseActivity +import com.asfoundation.wallet.ui.TransactionsActivity +import com.asfoundation.wallet.wallet_validation.ValidationInfo +import dagger.android.AndroidInjection +import kotlinx.android.synthetic.main.activity_wallet_validation.* +import kotlinx.android.synthetic.main.layout_referral_status.* + +class WalletValidationActivity : BaseActivity(), + WalletValidationView { + + private lateinit var presenter: WalletValidationPresenter + + private var minFrame = 0 + private var maxFrame = 30 + private var loopAnimation = -1 + + private val hasBeenInvitedFlow: Boolean by lazy { + intent.getBooleanExtra(HAS_BEEN_INVITED_FLOW, false) + } + + private val navigateToTransactionsOnSuccess: Boolean by lazy { + intent.getBooleanExtra(NAVIGATE_TO_TRANSACTIONS_ON_SUCCESS, true) + } + + private val navigateToTransactionsOnCancel: Boolean by lazy { + intent.getBooleanExtra(NAVIGATE_TO_TRANSACTIONS_ON_CANCEL, true) + } + + private val showToolbar: Boolean by lazy { + intent.getBooleanExtra(SHOW_TOOLBAR, false) + } + + private val previousContext: String by lazy { + intent.getStringExtra(PREVIOUS_CONTEXT) + } + + private val errorMessage: Int by lazy { + intent.getIntExtra(ERROR_MESSAGE, 0) + } + + + companion object { + const val FRAME_RATE = 30 + const val HAS_BEEN_INVITED_FLOW = "has_been_invited_flow" + const val NAVIGATE_TO_TRANSACTIONS_ON_SUCCESS = "navigate_to_transactions_on_success" + const val NAVIGATE_TO_TRANSACTIONS_ON_CANCEL = "navigate_to_transactions_on_cancel" + const val SHOW_TOOLBAR = "show_toolbar" + const val PREVIOUS_CONTEXT = "previous_context" + const val MIN_FRAME = "minFrame" + const val MAX_FRAME = "maxFrame" + const val LOOP_ANIMATION = "loopAnimation" + + @JvmStatic + fun newIntent(context: Context, hasBeenInvitedFlow: Boolean, + navigateToTransactionsOnSuccess: Boolean, navigateToTransactionsOnCancel: Boolean, + showToolbar: Boolean, previousContext: String): Intent { + return Intent(context, WalletValidationActivity::class.java).apply { + putExtra(HAS_BEEN_INVITED_FLOW, hasBeenInvitedFlow) + putExtra(NAVIGATE_TO_TRANSACTIONS_ON_SUCCESS, navigateToTransactionsOnSuccess) + putExtra(NAVIGATE_TO_TRANSACTIONS_ON_CANCEL, navigateToTransactionsOnCancel) + putExtra(SHOW_TOOLBAR, showToolbar) + putExtra(PREVIOUS_CONTEXT, previousContext) + } + } + + fun newIntent(context: Context, @StringRes error: Int): Intent { + return Intent(context, WalletValidationActivity::class.java).apply { + putExtra(HAS_BEEN_INVITED_FLOW, false) + putExtra(NAVIGATE_TO_TRANSACTIONS_ON_SUCCESS, false) + putExtra(NAVIGATE_TO_TRANSACTIONS_ON_CANCEL, false) + putExtra(SHOW_TOOLBAR, true) + putExtra(ERROR_MESSAGE, error) + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + outState.putInt(MIN_FRAME, minFrame) + outState.putInt(MAX_FRAME, maxFrame) + outState.putInt(LOOP_ANIMATION, loopAnimation) + } + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_wallet_validation) + presenter = WalletValidationPresenter(this) + + handleSavedInstance(savedInstanceState) + + setupUI() + + presenter.present(savedInstanceState != null) + } + + private fun handleSavedInstance(savedInstanceState: Bundle?) { + savedInstanceState?.let { + if (savedInstanceState.containsKey(MIN_FRAME)) { + minFrame = it.getInt(MIN_FRAME) + } + if (savedInstanceState.containsKey(MAX_FRAME)) { + maxFrame = it.getInt(MAX_FRAME) + } + if (savedInstanceState.containsKey(LOOP_ANIMATION)) { + loopAnimation = it.getInt(LOOP_ANIMATION) + } + } + } + + private fun setupUI() { + validation_progress_animation.setMinAndMaxFrame(minFrame, maxFrame) + validation_progress_animation.repeatCount = loopAnimation + validation_progress_animation.playAnimation() + scrollView.isHorizontalScrollBarEnabled = false + scrollView.isVerticalScrollBarEnabled = false + setupToolbar() + + if (errorMessage != 0) { + val intent = Intent().apply { putExtra(ERROR_MESSAGE, errorMessage) } + setResult(RESULT_CANCELED, intent) + } + } + + private fun setupToolbar() { + if (showToolbar) { + toolbar() + setTitle(getString(R.string.verification_settings_unverified_title)) + wallet_validation_toolbar.visibility = View.VISIBLE + } + } + + override fun showProgressAnimation() { + validation_progress_animation.visibility = View.VISIBLE + } + + override fun hideProgressAnimation() { + validation_progress_animation.visibility = View.INVISIBLE + } + + override fun showPhoneValidationView(countryCode: String?, phoneNumber: String?, + errorMessage: Int?, isSavedInstance: Boolean) { + if (!isSavedInstance) { + if (countryCode != null && phoneNumber != null) { + reverseAnimation(30, 60, 0) + } + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + PhoneValidationFragment.newInstance( + countryCode, phoneNumber, errorMessage, hasBeenInvitedFlow, previousContext)) + .commit() + + Handler().postDelayed({ + if (countryCode != null && phoneNumber != null) { + reverseAnimation(0, 30, -1) + } + }, 1000) + } + } + + override fun showCodeValidationView(countryCode: String, phoneNumber: String) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + CodeValidationFragment.newInstance(countryCode, phoneNumber, hasBeenInvitedFlow)) + .commit() + increaseAnimationFrames() + } + + override fun showCodeValidationView(validationInfo: ValidationInfo, errorMessage: Int) { + updateAnimation() + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, + CodeValidationFragment.newInstance(validationInfo, errorMessage, hasBeenInvitedFlow)) + .commit() + } + + override fun showLastStepAnimation() { + increaseAnimationFrames() + } + + override fun finishSuccessActivity() { + if (navigateToTransactionsOnSuccess) { + startActivity(TransactionsActivity.newIntent(this)) + } else if (errorMessage != 0) { + val intent = Intent().apply { putExtra(ERROR_MESSAGE, errorMessage) } + setResult(RESULT_OK, intent) + } + finish() + } + + override fun finishCancelActivity() { + if (navigateToTransactionsOnCancel) { + startActivity(TransactionsActivity.newIntent(this)) + } else if (errorMessage != 0) { + val intent = Intent().apply { putExtra(ERROR_MESSAGE, errorMessage) } + setResult(RESULT_CANCELED, intent) + } + finish() + } + + private fun updateAnimation() { + validation_progress_animation.removeAllAnimatorListeners() + validation_progress_animation.setMinAndMaxFrame(minFrame, maxFrame) + validation_progress_animation.repeatCount = loopAnimation + validation_progress_animation.addAnimatorListener(object : Animator.AnimatorListener { + override fun onAnimationRepeat(animation: Animator?) = Unit + + override fun onAnimationEnd(animation: Animator?) { + minFrame += FRAME_RATE + maxFrame += FRAME_RATE + loopAnimation = -1 + validation_progress_animation.setMinAndMaxFrame(minFrame, maxFrame) + validation_progress_animation.repeatCount = loopAnimation + validation_progress_animation.playAnimation() + } + + override fun onAnimationCancel(animation: Animator?) = Unit + + override fun onAnimationStart(animation: Animator?) = Unit + }) + validation_progress_animation.playAnimation() + } + + private fun increaseAnimationFrames() { + minFrame += FRAME_RATE + maxFrame += FRAME_RATE + loopAnimation = 0 + updateAnimation() + } + + override fun onDestroy() { + validation_progress_animation?.removeAllUpdateListeners() + validation_progress_animation?.removeAllLottieOnCompositionLoadedListener() + + referral_status_animation?.removeAllUpdateListeners() + referral_status_animation?.removeAllLottieOnCompositionLoadedListener() + + super.onDestroy() + } + + private fun reverseAnimation(minFrame: Int, maxFrame: Int, loop: Int) { + this.minFrame = minFrame + this.maxFrame = maxFrame + loopAnimation = loop + + validation_progress_animation.removeAllAnimatorListeners() + validation_progress_animation.setMinAndMaxFrame(this.minFrame, this.maxFrame) + validation_progress_animation.repeatCount = loopAnimation + validation_progress_animation.reverseAnimationSpeed() + validation_progress_animation.playAnimation() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/WalletValidationAnalytics.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/WalletValidationAnalytics.kt new file mode 100644 index 00000000000..f46daed3025 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/WalletValidationAnalytics.kt @@ -0,0 +1,59 @@ +package com.asfoundation.wallet.wallet_validation.generic + +import cm.aptoide.analytics.AnalyticsManager +import java.util.* + +class WalletValidationAnalytics(private val analyticsManager: AnalyticsManager) { + + fun sendPhoneVerificationEvent(action: String, context: String, status: String, error: String) { + val eventData = buildBaseDataMap(status, error) + + eventData[ACTION] = action + eventData[CONTEXT] = context + + analyticsManager.logEvent(eventData, WALLET_PHONE_NUMBER_VERIFICATION, + AnalyticsManager.Action.CLICK, + WALLET) + } + + fun sendCodeVerificationEvent(action: String) { + val eventData = HashMap() + + eventData[ACTION] = action + + analyticsManager.logEvent(eventData, WALLET_CODE_VERIFICATION, AnalyticsManager.Action.CLICK, + WALLET) + } + + + fun sendConfirmationEvent(status: String, error: String) { + val eventData = buildBaseDataMap(status, error) + + analyticsManager.logEvent(eventData, WALLET_VERIFICATION_CONFIRMATION, + AnalyticsManager.Action.CLICK, + WALLET) + } + + + private fun buildBaseDataMap(status: String, error: String): HashMap { + val eventData = HashMap() + + eventData[STATUS] = status + if (error.isNotEmpty()) eventData[ERROR_DETAILS] = error + + return eventData + } + + companion object { + const val WALLET_PHONE_NUMBER_VERIFICATION = "wallet_phone_number_verification" + const val WALLET_CODE_VERIFICATION = "wallet_code_verification" + const val WALLET_VERIFICATION_CONFIRMATION = "wallet_verification_confirmation" + + private const val ACTION = "action" + private const val STATUS = "status" + private const val CONTEXT = "context" + private const val ERROR_DETAILS = "error_details" + + private const val WALLET = "wallet" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/WalletValidationPresenter.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/WalletValidationPresenter.kt new file mode 100644 index 00000000000..9f6b5465735 --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/WalletValidationPresenter.kt @@ -0,0 +1,9 @@ +package com.asfoundation.wallet.wallet_validation.generic + +class WalletValidationPresenter(private val view: WalletValidationView) { + + fun present(isSavedInstance: Boolean) { + view.showPhoneValidationView(isSavedInstance = isSavedInstance) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/WalletValidationView.kt b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/WalletValidationView.kt new file mode 100644 index 00000000000..17f625adf4f --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/wallet_validation/generic/WalletValidationView.kt @@ -0,0 +1,23 @@ +package com.asfoundation.wallet.wallet_validation.generic + +import com.asfoundation.wallet.wallet_validation.ValidationInfo + +interface WalletValidationView { + + fun showPhoneValidationView(countryCode: String? = null, phoneNumber: String? = null, + errorMessage: Int? = null, isSavedInstance: Boolean = false) + + fun showCodeValidationView(countryCode: String, phoneNumber: String) + + fun showCodeValidationView(validationInfo: ValidationInfo, errorMessage: Int) + + fun finishSuccessActivity() + + fun showLastStepAnimation() + + fun showProgressAnimation() + + fun hideProgressAnimation() + + fun finishCancelActivity() +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/widget/AddWalletView.java b/app/src/main/java/com/asfoundation/wallet/widget/AddWalletView.java deleted file mode 100644 index 107e160db62..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/widget/AddWalletView.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.asfoundation.wallet.widget; - -import android.content.Context; -import android.support.annotation.LayoutRes; -import android.support.annotation.NonNull; -import android.support.v4.view.PagerAdapter; -import android.support.v4.view.ViewPager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; -import com.asf.wallet.R; - -public class AddWalletView extends FrameLayout implements View.OnClickListener { - private OnNewWalletClickListener onNewWalletClickListener; - private OnImportWalletClickListener onImportWalletClickListener; - - public AddWalletView(Context context) { - this(context, R.layout.layout_dialog_add_account); - } - - public AddWalletView(Context context, @LayoutRes int layoutId) { - super(context); - - init(layoutId); - } - - private void init(@LayoutRes int layoutId) { - LayoutInflater.from(getContext()) - .inflate(layoutId, this, true); - findViewById(R.id.new_account_action).setOnClickListener(this); - findViewById(R.id.import_account_action).setOnClickListener(this); - - ViewPager viewPager = findViewById(R.id.intro); - if (viewPager != null) { - viewPager.setPageTransformer(false, new DepthPageTransformer()); - viewPager.setAdapter(new IntroPagerAdapter()); - } - } - - @Override public void onClick(View view) { - switch (view.getId()) { - case R.id.new_account_action: { - if (onNewWalletClickListener != null) { - onNewWalletClickListener.onNewWallet(view); - } - } - break; - case R.id.import_account_action: { - if (onImportWalletClickListener != null) { - onImportWalletClickListener.onImportWallet(view); - } - } - break; - } - } - - public void setOnNewWalletClickListener(OnNewWalletClickListener onNewWalletClickListener) { - this.onNewWalletClickListener = onNewWalletClickListener; - } - - public void setOnImportWalletClickListener( - OnImportWalletClickListener onImportWalletClickListener) { - this.onImportWalletClickListener = onImportWalletClickListener; - } - - public interface OnNewWalletClickListener { - void onNewWallet(View view); - } - - public interface OnImportWalletClickListener { - void onImportWallet(View view); - } - - private static class IntroPagerAdapter extends PagerAdapter { - private int[] titles = new int[] { - R.string.intro_title_first_page, R.string.welcome_erc20_label_title, - R.string.intro_title_second_page, R.string.intro_title_third_page, - }; - private int[] messages = new int[] { - R.string.intro_message_first_page, R.string.welcome_erc20_label_description, - R.string.intro_message_second_page, R.string.intro_message_third_page, - }; - private int[] images = new int[] { - R.drawable.onboarding_asf_ic, R.drawable.onboarding_erc20, R.mipmap.onboarding_open_source, - R.mipmap.onboarding_rocket - }; - - @Override public int getCount() { - return titles.length; - } - - @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { - View view = LayoutInflater.from(container.getContext()) - .inflate(R.layout.layout_page_intro, container, false); - ((TextView) view.findViewById(R.id.title)).setText(titles[position]); - ((TextView) view.findViewById(R.id.message)).setText(messages[position]); - ((ImageView) view.findViewById(R.id.img)).setImageResource(images[position]); - container.addView(view); - return view; - } - - @Override - public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { - container.removeView((View) object); - } - - @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { - return view == object; - } - } - - private static class DepthPageTransformer implements ViewPager.PageTransformer { - private static final float MIN_SCALE = 0.75f; - - public void transformPage(View view, float position) { - int pageWidth = view.getWidth(); - - if (position < -1) { // [-Infinity,-1) - // This page is way off-screen to the left. - view.setAlpha(0); - } else if (position <= 0) { // [-1,0] - // Use the default slide transition when moving to the left page - view.setAlpha(1); - view.setTranslationX(0); - view.setScaleX(1); - view.setScaleY(1); - } else if (position <= 1) { // (0,1] - // Fade the page out. - view.setAlpha(1 - position); - - // Counteract the default slide transition - view.setTranslationX(pageWidth * -position); - - // Scale the page down (between MIN_SCALE and 1) - float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position)); - view.setScaleX(scaleFactor); - view.setScaleY(scaleFactor); - } else { // (1,+Infinity] - // This page is way off-screen to the right. - view.setAlpha(0); - } - } - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/widget/BackupView.java b/app/src/main/java/com/asfoundation/wallet/widget/BackupView.java deleted file mode 100644 index 81dbbae5b0b..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/widget/BackupView.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.asfoundation.wallet.widget; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.view.LayoutInflater; -import android.widget.EditText; -import android.widget.FrameLayout; -import com.asf.wallet.R; - -public class BackupView extends FrameLayout { - private EditText password; - - public BackupView(@NonNull Context context) { - super(context); - - init(); - } - - private void init() { - LayoutInflater.from(getContext()) - .inflate(R.layout.layout_dialog_backup, this, true); - password = findViewById(R.id.password); - } - - public String getPassword() { - return password.getText() - .toString(); - } - - public void showKeyBoard() { - password.requestFocus(); - } - - @Override protected void onAttachedToWindow() { - super.onAttachedToWindow(); - showKeyBoard(); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/widget/BackupWarningView.java b/app/src/main/java/com/asfoundation/wallet/widget/BackupWarningView.java deleted file mode 100644 index 3f2cd54ce4b..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/widget/BackupWarningView.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.asfoundation.wallet.widget; - -import android.content.Context; -import android.support.annotation.LayoutRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.content.ContextCompat; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.FrameLayout; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.ui.widget.OnBackupClickListener; - -public class BackupWarningView extends FrameLayout implements View.OnClickListener { - - private OnBackupClickListener onPositiveClickListener; - private OnBackupClickListener onNegativeClickListener; - private Wallet wallet; - - public BackupWarningView(@NonNull Context context) { - this(context, null); - } - - public BackupWarningView(@NonNull Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public BackupWarningView(@NonNull Context context, @Nullable AttributeSet attrs, - int defStyleAttr) { - super(context, attrs, defStyleAttr); - - init(R.layout.layout_dialog_warning_backup); - } - - private void init(@LayoutRes int layoutId) { - setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white)); - LayoutInflater.from(getContext()) - .inflate(layoutId, this, true); - findViewById(R.id.backup_action).setOnClickListener(this); - /* Disabled due to https://github.com/TrustWallet/trust-wallet-android/issues/107 - * findViewById(R.id.later_action).setOnClickListener(this); - */ - } - - @Override public void onClick(View v) { - switch (v.getId()) { - case R.id.backup_action: { - if (onPositiveClickListener != null) { - onPositiveClickListener.onBackupClick(v, wallet); - } - } - break; - case R.id.later_action: { - if (onNegativeClickListener != null) { - onNegativeClickListener.onBackupClick(v, wallet); - } - } - } - } - - public void setOnNegativeClickListener(OnBackupClickListener onNegativeClickListener) { - this.onNegativeClickListener = onNegativeClickListener; - } - - public void setOnPositiveClickListener(OnBackupClickListener onPositiveClickListener) { - this.onPositiveClickListener = onPositiveClickListener; - } - - public void show(Wallet wallet) { - setVisibility(VISIBLE); - this.wallet = wallet; - } - - public void hide() { - setVisibility(GONE); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/widget/CardHeaderTransformation.java b/app/src/main/java/com/asfoundation/wallet/widget/CardHeaderTransformation.java new file mode 100644 index 00000000000..f6ec02022af --- /dev/null +++ b/app/src/main/java/com/asfoundation/wallet/widget/CardHeaderTransformation.java @@ -0,0 +1,110 @@ +package com.asfoundation.wallet.widget; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.RectF; +import android.graphics.Shader; +import android.os.Build; +import androidx.annotation.NonNull; +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; +import java.security.MessageDigest; + +public class CardHeaderTransformation extends BitmapTransformation { + private static final byte[] ID_BYTES = "card_header".getBytes(CHARSET); + private final int radius; + int margin = 0; + + public CardHeaderTransformation(int radius) { + this.radius = radius; + } + + @NonNull private static Bitmap.Config getAlphaSafeConfig(@NonNull Bitmap inBitmap) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (Bitmap.Config.RGBA_F16.equals(inBitmap.getConfig())) { + return Bitmap.Config.RGBA_F16; + } + } + + return Bitmap.Config.ARGB_8888; + } + + private static Bitmap getAlphaSafeBitmap(@NonNull BitmapPool pool, + @NonNull Bitmap maybeAlphaSafe) { + Bitmap.Config safeConfig = getAlphaSafeConfig(maybeAlphaSafe); + if (safeConfig.equals(maybeAlphaSafe.getConfig())) { + return maybeAlphaSafe; + } + + Bitmap argbBitmap = pool.get(maybeAlphaSafe.getWidth(), maybeAlphaSafe.getHeight(), safeConfig); + new Canvas(argbBitmap).drawBitmap(maybeAlphaSafe, 0, 0, null); + return argbBitmap; + } + + private static void clear(Canvas canvas) { + canvas.setBitmap(null); + } + + public Bitmap transform(Bitmap toTransform) { + final Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setShader(new BitmapShader(toTransform, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); + + Bitmap output = Bitmap.createBitmap(toTransform.getWidth(), toTransform.getHeight(), + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + canvas.drawRect(new RectF(margin, margin + radius, toTransform.getWidth() - margin, + toTransform.getHeight() - margin), paint); + canvas.drawRoundRect(new RectF(margin, margin, toTransform.getWidth() - margin, + toTransform.getHeight() - margin), radius, radius, paint); + + if (toTransform != output) { + toTransform.recycle(); + } + + return output; + } + + @Override + protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap inBitmap, int outWidth, + int outHeight) { + + int width = inBitmap.getWidth(); + int height = inBitmap.getHeight(); + float right = width - margin; + float bottom = height - margin; + + // Alpha is required for this transformation. + Bitmap.Config safeConfig = getAlphaSafeConfig(inBitmap); + Bitmap toTransform = getAlphaSafeBitmap(pool, inBitmap); + Bitmap result = pool.get(toTransform.getWidth(), toTransform.getHeight(), safeConfig); + + result.setHasAlpha(true); + + BitmapShader shader = + new BitmapShader(toTransform, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setShader(shader); + Canvas canvas = new Canvas(result); + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + canvas.drawRoundRect(new RectF(margin, margin, right, margin + (radius * 2)), radius, radius, + paint); + canvas.drawRect(new RectF(margin, margin + radius, right, bottom), paint); + clear(canvas); + + if (!toTransform.equals(inBitmap)) { + pool.put(toTransform); + } + + return result; + } + + @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { + messageDigest.update(ID_BYTES); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/widget/CircleTransformation.java b/app/src/main/java/com/asfoundation/wallet/widget/CircleTransformation.java deleted file mode 100644 index 89903f92566..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/widget/CircleTransformation.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.asfoundation.wallet.widget; - -import android.graphics.Bitmap; -import android.graphics.BitmapShader; -import android.graphics.Canvas; -import android.graphics.Paint; -import com.squareup.picasso.Transformation; - -/** - * Created by Joao Raimundo on 19/05/2018. - */ -public class CircleTransformation implements Transformation { - @Override public Bitmap transform(Bitmap source) { - int size = Math.min(source.getWidth(), source.getHeight()); - - int x = (source.getWidth() - size) / 2; - int y = (source.getHeight() - size) / 2; - - Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size); - if (squaredBitmap != source) { - source.recycle(); - } - - Bitmap bitmap = Bitmap.createBitmap(size, size, source.getConfig()); - - Canvas canvas = new Canvas(bitmap); - Paint paint = new Paint(); - BitmapShader shader = - new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP); - paint.setShader(shader); - paint.setAntiAlias(true); - - float r = size / 2f; - float radius = size/2.5f; - canvas.drawCircle(r, r, radius, paint); - - squaredBitmap.recycle(); - return bitmap; - } - - @Override public String key() { - return "circle"; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/widget/DepositView.java b/app/src/main/java/com/asfoundation/wallet/widget/DepositView.java deleted file mode 100644 index e82374be563..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/widget/DepositView.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.asfoundation.wallet.widget; - -import android.content.Context; -import android.net.Uri; -import android.support.annotation.LayoutRes; -import android.support.annotation.NonNull; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.FrameLayout; -import com.asf.wallet.R; -import com.asfoundation.wallet.entity.Wallet; -import com.asfoundation.wallet.ui.widget.OnDepositClickListener; - -import static com.asfoundation.wallet.C.CHANGELLY_REF_ID; -import static com.asfoundation.wallet.C.COINBASE_WIDGET_CODE; -import static com.asfoundation.wallet.C.ETH_SYMBOL; -import static com.asfoundation.wallet.C.SHAPESHIFT_KEY; - -public class DepositView extends FrameLayout implements View.OnClickListener { - - private static final Uri coinbaseri = Uri.parse("https://buy.coinbase.com/widget") - .buildUpon() - .appendQueryParameter("code", COINBASE_WIDGET_CODE) - .appendQueryParameter("amount", "0") - // .address={address} - .appendQueryParameter("crypto_currency", ETH_SYMBOL) - .build(); - private static final Uri shapeshiftUri = Uri.parse("https://shapeshift.io/shifty.html") - .buildUpon() - .appendQueryParameter("apiKey", SHAPESHIFT_KEY) - .appendQueryParameter("amount", "0") - // ?destination=\(address) - .appendQueryParameter("output", ETH_SYMBOL) - .build(); - - private static final Uri changellyteUri = - Uri.parse("https://changelly.com/widget/v1?auth=email&from=BTC") - .buildUpon() - .appendQueryParameter("to", ETH_SYMBOL) - .appendQueryParameter("merchant_id", CHANGELLY_REF_ID) - // address=\\(address) - .appendQueryParameter("amount", "0") - .appendQueryParameter("ref_id", CHANGELLY_REF_ID) - .appendQueryParameter("color", "00cf70") - .build(); - - private OnDepositClickListener onDepositClickListener; - @NonNull private Wallet wallet; - - public DepositView(Context context, @NonNull Wallet wallet) { - this(context, R.layout.layout_dialog_deposit, wallet); - } - - public DepositView(Context context, @LayoutRes int layoutId, @NonNull Wallet wallet) { - super(context); - - init(layoutId, wallet); - } - - private void init(@LayoutRes int layoutId, @NonNull Wallet wallet) { - this.wallet = wallet; - LayoutInflater.from(getContext()) - .inflate(layoutId, this, true); - findViewById(R.id.action_coinbase).setOnClickListener(this); - findViewById(R.id.action_shapeshift).setOnClickListener(this); - findViewById(R.id.action_changelly).setOnClickListener(this); - } - - @Override public void onClick(View v) { - Uri uri; - switch (v.getId()) { - case R.id.action_shapeshift: { - uri = shapeshiftUri.buildUpon() - .appendQueryParameter("destination", wallet.address) - .build(); - } - break; - case R.id.action_changelly: { - uri = changellyteUri.buildUpon() - .appendQueryParameter("address", wallet.address) - .build(); - } - break; - default: - case R.id.action_coinbase: { - uri = coinbaseri.buildUpon() - .appendQueryParameter("address", wallet.address) - .build(); - } - break; - } - if (onDepositClickListener != null) { - onDepositClickListener.onDepositClick(v, uri); - } - } - - public void setOnDepositClickListener(OnDepositClickListener onDepositClickListener) { - this.onDepositClickListener = onDepositClickListener; - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/widget/EmptyTransactionsView.java b/app/src/main/java/com/asfoundation/wallet/widget/EmptyTransactionsView.java index 84cbcf1bff6..c0f46c63824 100644 --- a/app/src/main/java/com/asfoundation/wallet/widget/EmptyTransactionsView.java +++ b/app/src/main/java/com/asfoundation/wallet/widget/EmptyTransactionsView.java @@ -1,29 +1,64 @@ package com.asfoundation.wallet.widget; import android.content.Context; -import android.support.annotation.NonNull; import android.view.LayoutInflater; -import android.widget.Button; import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.viewpager.widget.ViewPager; import com.asf.wallet.R; -import com.asfoundation.wallet.entity.NetworkInfo; +import com.asfoundation.wallet.ui.TransactionsActivity; +import com.asfoundation.wallet.ui.widget.adapter.EmptyTransactionPagerAdapter; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.subjects.PublishSubject; public class EmptyTransactionsView extends FrameLayout { - private final Button airdropButton; + private static final int MAX_BONUS_STRING_RESOURCE = R.string.gamification_home_body; + private static final int NUMBER_PAGES = 2; + private final int[] body = { R.string.home_empty_discover_apps_body, MAX_BONUS_STRING_RESOURCE }; - public EmptyTransactionsView(@NonNull Context context, OnClickListener onClickListener) { + public EmptyTransactionsView(@NonNull Context context, @NonNull String bonus, + PublishSubject emptyTransactionsSubject, TransactionsActivity transactionsActivity, + CompositeDisposable disposables) { super(context); LayoutInflater.from(getContext()) .inflate(R.layout.layout_empty_transactions, this, true); + ViewPager viewPager = findViewById(R.id.empty_transactions_viewpager); + int[] action = { R.string.home_empty_discover_apps_button, R.string.gamification_home_button }; + int[] animation = + { R.raw.carousel_empty_screen_animation, R.raw.transactions_empty_screen_animation }; + EmptyTransactionPagerAdapter pageAdapter = + new EmptyTransactionPagerAdapter(animation, transformBodyResourceToString(body, bonus), + action, NUMBER_PAGES, viewPager, emptyTransactionsSubject); + pageAdapter.randomizeCarouselContent(); + viewPager.setAdapter(pageAdapter); - airdropButton = findViewById(R.id.action_air_drop); - findViewById(R.id.action_learn_more).setOnClickListener(onClickListener); - airdropButton.setOnClickListener(onClickListener); + disposables.add(transactionsActivity.getEmptyTransactionsScreenClick() + .doOnNext(string -> { + if (string.equals(EmptyTransactionPagerAdapter.CAROUSEL_GAMIFICATION)) { + transactionsActivity.navigateToPromotions(false); + } + if (string.equals(EmptyTransactionPagerAdapter.CAROUSEL_TOP_APPS)) { + transactionsActivity.navigateToTopApps(); + } + }) + .subscribe()); } - public void setAirdropButtonEnable(boolean enabled) { - airdropButton.setEnabled(enabled); + private String setMaxBonusOnString(String bonus) { + return getResources().getString(MAX_BONUS_STRING_RESOURCE, bonus); } -} + + private String[] transformBodyResourceToString(int[] bodyArray, String maxBonus) { + String[] bodyContent = new String[NUMBER_PAGES]; + for (int i = 0; i < bodyArray.length; i++) { + if (bodyArray[i] == MAX_BONUS_STRING_RESOURCE) { + bodyContent[i] = setMaxBonusOnString(maxBonus); + } else { + bodyContent[i] = getResources().getString(body[i]); + } + } + return bodyContent; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/widget/HelperTextInputLayout.java b/app/src/main/java/com/asfoundation/wallet/widget/HelperTextInputLayout.java deleted file mode 100644 index 5626895adaa..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/widget/HelperTextInputLayout.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.asfoundation.wallet.widget; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.support.design.widget.TextInputLayout; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; -import android.support.v4.view.animation.FastOutSlowInInterpolator; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Interpolator; -import android.widget.EditText; -import android.widget.TextView; -import com.asf.wallet.R; - -/** - * TextInputLayout temporary workaround for helper text showing - * https://gist.github.com/drstranges/1a86965f582f610244d6 - */ -public class HelperTextInputLayout extends TextInputLayout { - - static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator(); - - private CharSequence mHelperText; - private ColorStateList mHelperTextColor; - private boolean mHelperTextEnabled = false; - private boolean mErrorEnabled = false; - private TextView mHelperView; - private int mHelperTextAppearance = R.style.HelperTextAppearance; - - public HelperTextInputLayout(Context _context) { - super(_context); - } - - public HelperTextInputLayout(Context _context, AttributeSet _attrs) { - super(_context, _attrs); - - final TypedArray a = - getContext().obtainStyledAttributes(_attrs, R.styleable.HelperTextInputLayout, 0, 0); - try { - mHelperTextColor = a.getColorStateList(R.styleable.HelperTextInputLayout_helperTextColor); - mHelperText = a.getText(R.styleable.HelperTextInputLayout_helperText); - } finally { - a.recycle(); - } - } - - @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { - super.addView(child, index, params); - if (child instanceof EditText) { - if (!TextUtils.isEmpty(mHelperText)) { - setHelperText(mHelperText); - } - } - } - - @Override public void setErrorEnabled(boolean _enabled) { - if (mErrorEnabled == _enabled) return; - mErrorEnabled = _enabled; - if (_enabled && mHelperTextEnabled) { - setHelperTextEnabled(false); - } - - super.setErrorEnabled(_enabled); - - if (!(_enabled || TextUtils.isEmpty(mHelperText))) { - setHelperText(mHelperText); - } - } - - public int getHelperTextAppearance() { - return mHelperTextAppearance; - } - - public void setHelperTextAppearance(int _helperTextAppearanceResId) { - mHelperTextAppearance = _helperTextAppearanceResId; - } - - public void setHelperTextColor(ColorStateList _helperTextColor) { - mHelperTextColor = _helperTextColor; - } - - public void setHelperTextEnabled(boolean _enabled) { - if (mHelperTextEnabled == _enabled) return; - if (_enabled && mErrorEnabled) { - setErrorEnabled(false); - } - if (this.mHelperTextEnabled != _enabled) { - if (_enabled) { - this.mHelperView = new TextView(this.getContext()); - this.mHelperView.setTextAppearance(this.getContext(), this.mHelperTextAppearance); - if (mHelperTextColor != null) { - this.mHelperView.setTextColor(mHelperTextColor); - } - this.mHelperView.setVisibility(INVISIBLE); - this.addView(this.mHelperView); - if (this.mHelperView != null) { - ViewCompat.setPaddingRelative(this.mHelperView, ViewCompat.getPaddingStart(getEditText()), - 0, ViewCompat.getPaddingEnd(getEditText()), getEditText().getPaddingBottom()); - } - } else { - this.removeView(this.mHelperView); - this.mHelperView = null; - } - - this.mHelperTextEnabled = _enabled; - } - } - - public void setHelperText(CharSequence _helperText) { - mHelperText = _helperText; - if (!this.mHelperTextEnabled) { - if (TextUtils.isEmpty(mHelperText)) { - return; - } - this.setHelperTextEnabled(true); - } - - if (!TextUtils.isEmpty(mHelperText)) { - this.mHelperView.setText(mHelperText); - this.mHelperView.setVisibility(VISIBLE); - ViewCompat.setAlpha(this.mHelperView, 0.0F); - ViewCompat.animate(this.mHelperView) - .alpha(1.0F) - .setDuration(200L) - .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) - .setListener(null) - .start(); - } else if (this.mHelperView.getVisibility() == VISIBLE) { - ViewCompat.animate(this.mHelperView) - .alpha(0.0F) - .setDuration(200L) - .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) - .setListener(new ViewPropertyAnimatorListenerAdapter() { - public void onAnimationEnd(View view) { - mHelperView.setText(null); - mHelperView.setVisibility(INVISIBLE); - } - }) - .start(); - } - this.sendAccessibilityEvent(2048); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/asfoundation/wallet/widget/ImageBehavior.java b/app/src/main/java/com/asfoundation/wallet/widget/ImageBehavior.java deleted file mode 100644 index 237da3a0c4c..00000000000 --- a/app/src/main/java/com/asfoundation/wallet/widget/ImageBehavior.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.asfoundation.wallet.widget; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.content.Context; -import android.os.Parcelable; -import android.support.design.widget.AppBarLayout; -import android.support.design.widget.CoordinatorLayout; -import android.support.v4.view.animation.FastOutSlowInInterpolator; -import android.util.AttributeSet; -import android.view.View; -import android.view.animation.Interpolator; - -/** - * Created by Joao Raimundo on 20/05/2018. - */ -@SuppressWarnings("unused") -public class ImageBehavior extends CoordinatorLayout.Behavior { - private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator(); - - public ImageBehavior(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { - if (dependency instanceof AppBarLayout) { - - animate(dependency, child); - return true; - } - return false; - } - - private void animate(final View dependency, final View view) { - float ratio = getCollapsingRation(dependency); - view.animate() - .cancel(); - - view.animate() - .scaleX(ratio) - .scaleY(ratio) - .setInterpolator(INTERPOLATOR) - .setDuration(1) - .setListener(new AnimatorListenerAdapter() { - @Override public void onAnimationStart(Animator animator) { - if (view.getVisibility() == View.INVISIBLE) { - view.setVisibility(View.VISIBLE); - } - } - - @Override public void onAnimationEnd(Animator animator) { - if (ratio == 0) { - view.setVisibility(View.VISIBLE); - } - } - }) - .start(); - } - - private float getCollapsingRation(View dependency) { - float height = dependency.getHeight(); - float bottom = dependency.getBottom(); - float totalScroll = ((AppBarLayout) dependency).getTotalScrollRange(); - - return 1 - ((height - bottom) / totalScroll); - } -} diff --git a/app/src/main/java/com/asfoundation/wallet/widget/SystemView.java b/app/src/main/java/com/asfoundation/wallet/widget/SystemView.java index ff0796ceefa..5ecb855e25c 100644 --- a/app/src/main/java/com/asfoundation/wallet/widget/SystemView.java +++ b/app/src/main/java/com/asfoundation/wallet/widget/SystemView.java @@ -1,12 +1,6 @@ package com.asfoundation.wallet.widget; import android.content.Context; -import android.support.annotation.LayoutRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.util.AttributeSet; import android.view.Gravity; @@ -16,7 +10,12 @@ import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.asf.wallet.R; +import com.google.android.material.snackbar.Snackbar; public class SystemView extends FrameLayout implements View.OnClickListener { private ProgressBar progress; @@ -66,7 +65,7 @@ public void attachRecyclerView(@Nullable RecyclerView recyclerView) { this.recyclerView = recyclerView; } - public void hide() { + private void hide() { hideAllComponents(); setVisibility(GONE); } @@ -81,6 +80,13 @@ private void hideAllComponents() { setVisibility(VISIBLE); } + private void hideProgressBar() { + if (swipeRefreshLayout != null && swipeRefreshLayout.isRefreshing()) { + swipeRefreshLayout.setRefreshing(false); + } + progress.setVisibility(GONE); + } + public void showProgress(boolean shouldShow) { if (shouldShow && swipeRefreshLayout != null && swipeRefreshLayout.isRefreshing()) { return; @@ -98,7 +104,7 @@ public void showProgress(boolean shouldShow) { progress.setVisibility(VISIBLE); } } else { - hide(); + hideProgressBar(); } } @@ -125,19 +131,6 @@ public void showError(@Nullable String message, } } - public void showEmpty() { - showEmpty(""); - } - - public void showEmpty(@NonNull String message) { - showError(message, null); - } - - public void showEmpty(@LayoutRes int emptyLayout) { - showEmpty(LayoutInflater.from(getContext()) - .inflate(emptyLayout, emptyBox, false)); - } - public void showEmpty(View view) { hideAllComponents(); LayoutParams lp = diff --git a/app/src/main/res/anim/bounce_animation.xml b/app/src/main/res/anim/bounce_animation.xml new file mode 100644 index 00000000000..a9e29d0392a --- /dev/null +++ b/app/src/main/res/anim/bounce_animation.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fade_in_animation.xml b/app/src/main/res/anim/fade_in_animation.xml new file mode 100644 index 00000000000..a0f0ecedb18 --- /dev/null +++ b/app/src/main/res/anim/fade_in_animation.xml @@ -0,0 +1,5 @@ + + diff --git a/app/src/main/res/anim/fade_out_animation.xml b/app/src/main/res/anim/fade_out_animation.xml new file mode 100644 index 00000000000..5ef0ff87bd0 --- /dev/null +++ b/app/src/main/res/anim/fade_out_animation.xml @@ -0,0 +1,5 @@ + + diff --git a/app/src/main/res/anim/fast_100s_fade_out_animation.xml b/app/src/main/res/anim/fast_100s_fade_out_animation.xml new file mode 100644 index 00000000000..f7ebe071ac2 --- /dev/null +++ b/app/src/main/res/anim/fast_100s_fade_out_animation.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fast_fade_in_animation.xml b/app/src/main/res/anim/fast_fade_in_animation.xml new file mode 100644 index 00000000000..b549889e34e --- /dev/null +++ b/app/src/main/res/anim/fast_fade_in_animation.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fast_fade_out_animation.xml b/app/src/main/res/anim/fast_fade_out_animation.xml new file mode 100644 index 00000000000..b3eb594a468 --- /dev/null +++ b/app/src/main/res/anim/fast_fade_out_animation.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_fade_in_animation.xml b/app/src/main/res/anim/fragment_fade_in_animation.xml new file mode 100644 index 00000000000..7967c2b7c50 --- /dev/null +++ b/app/src/main/res/anim/fragment_fade_in_animation.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_fade_out_animation.xml b/app/src/main/res/anim/fragment_fade_out_animation.xml new file mode 100644 index 00000000000..e66d258532e --- /dev/null +++ b/app/src/main/res/anim/fragment_fade_out_animation.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_slide_down.xml b/app/src/main/res/anim/fragment_slide_down.xml new file mode 100644 index 00000000000..38d22d70902 --- /dev/null +++ b/app/src/main/res/anim/fragment_slide_down.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_slide_up.xml b/app/src/main/res/anim/fragment_slide_up.xml new file mode 100644 index 00000000000..3b7a3d8636b --- /dev/null +++ b/app/src/main/res/anim/fragment_slide_up.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/grow_animation.xml b/app/src/main/res/anim/grow_animation.xml new file mode 100644 index 00000000000..50a89734606 --- /dev/null +++ b/app/src/main/res/anim/grow_animation.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/rebounce_animation.xml b/app/src/main/res/anim/rebounce_animation.xml new file mode 100644 index 00000000000..630735344bc --- /dev/null +++ b/app/src/main/res/anim/rebounce_animation.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/shrink_animation.xml b/app/src/main/res/anim/shrink_animation.xml new file mode 100644 index 00000000000..dd22cf7c987 --- /dev/null +++ b/app/src/main/res/anim/shrink_animation.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/billing_stroke_color.xml b/app/src/main/res/color/billing_stroke_color.xml new file mode 100644 index 00000000000..807565eccb6 --- /dev/null +++ b/app/src/main/res/color/billing_stroke_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/billing_switch_color.xml b/app/src/main/res/color/billing_switch_color.xml new file mode 100644 index 00000000000..a4fb7666c29 --- /dev/null +++ b/app/src/main/res/color/billing_switch_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/box_stroke_color.xml b/app/src/main/res/color/box_stroke_color.xml new file mode 100644 index 00000000000..e9b1d4154fd --- /dev/null +++ b/app/src/main/res/color/box_stroke_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/level_text_color.xml b/app/src/main/res/color/level_text_color.xml new file mode 100644 index 00000000000..df4fff9df82 --- /dev/null +++ b/app/src/main/res/color/level_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/payment_option_text_color.xml b/app/src/main/res/color/payment_option_text_color.xml new file mode 100644 index 00000000000..8b55f54a3f9 --- /dev/null +++ b/app/src/main/res/color/payment_option_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/radio_button_color.xml b/app/src/main/res/color/radio_button_color.xml new file mode 100644 index 00000000000..903d6f46f78 --- /dev/null +++ b/app/src/main/res/color/radio_button_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/radio_button_text_color.xml b/app/src/main/res/color/radio_button_text_color.xml new file mode 100644 index 00000000000..d1d42753e0e --- /dev/null +++ b/app/src/main/res/color/radio_button_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/check_mark.png b/app/src/main/res/drawable-hdpi/check_mark.png deleted file mode 100644 index 9ef58ae6ead..00000000000 Binary files a/app/src/main/res/drawable-hdpi/check_mark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_deposit.png b/app/src/main/res/drawable-hdpi/ic_action_deposit.png deleted file mode 100644 index bccc258b2b3..00000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_action_deposit.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_description_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_description_black_24dp.png deleted file mode 100644 index 71829f9ba39..00000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_description_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_facebook.png b/app/src/main/res/drawable-hdpi/ic_facebook.png deleted file mode 100644 index b311942a43e..00000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_facebook.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_refresh.png b/app/src/main/res/drawable-hdpi/ic_refresh.png deleted file mode 100644 index 449d46668a3..00000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_refresh.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/icon_alpha_version.png b/app/src/main/res/drawable-hdpi/icon_alpha_version.png deleted file mode 100644 index 52433ee210f..00000000000 Binary files a/app/src/main/res/drawable-hdpi/icon_alpha_version.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/icon_asf_lightgrey.png b/app/src/main/res/drawable-hdpi/icon_asf_lightgrey.png deleted file mode 100644 index 5247277db13..00000000000 Binary files a/app/src/main/res/drawable-hdpi/icon_asf_lightgrey.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/onboarding_asf_ic.png b/app/src/main/res/drawable-hdpi/onboarding_asf_ic.png deleted file mode 100644 index bc9aa4b9502..00000000000 Binary files a/app/src/main/res/drawable-hdpi/onboarding_asf_ic.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/settings_rpc_server.png b/app/src/main/res/drawable-hdpi/settings_rpc_server.png deleted file mode 100644 index 69f5f261847..00000000000 Binary files a/app/src/main/res/drawable-hdpi/settings_rpc_server.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/token_icon.png b/app/src/main/res/drawable-hdpi/token_icon.png deleted file mode 100644 index 9a3d78efca1..00000000000 Binary files a/app/src/main/res/drawable-hdpi/token_icon.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/ccp_background.xml b/app/src/main/res/drawable-ldpi/ccp_background.xml new file mode 100644 index 00000000000..fe558268ba5 --- /dev/null +++ b/app/src/main/res/drawable-ldpi/ccp_background.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-mdpi/check_mark.png b/app/src/main/res/drawable-mdpi/check_mark.png deleted file mode 100644 index f9d61943771..00000000000 Binary files a/app/src/main/res/drawable-mdpi/check_mark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_deposit.png b/app/src/main/res/drawable-mdpi/ic_action_deposit.png deleted file mode 100644 index 5d7b81f149f..00000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_action_deposit.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_description_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_description_black_24dp.png deleted file mode 100644 index a39e27f47b6..00000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_description_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_facebook.png b/app/src/main/res/drawable-mdpi/ic_facebook.png deleted file mode 100644 index aa7be92fd28..00000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_facebook.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_refresh.png b/app/src/main/res/drawable-mdpi/ic_refresh.png deleted file mode 100644 index 8ce208ce95e..00000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_refresh.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_share_white_24dp.xml b/app/src/main/res/drawable-mdpi/ic_share_white_24dp.xml deleted file mode 100644 index a54ee81be53..00000000000 --- a/app/src/main/res/drawable-mdpi/ic_share_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-mdpi/icon_alpha_version.png b/app/src/main/res/drawable-mdpi/icon_alpha_version.png deleted file mode 100644 index b7d129cc7a8..00000000000 Binary files a/app/src/main/res/drawable-mdpi/icon_alpha_version.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/icon_asf_lightgrey.png b/app/src/main/res/drawable-mdpi/icon_asf_lightgrey.png deleted file mode 100644 index ed06f5edb86..00000000000 Binary files a/app/src/main/res/drawable-mdpi/icon_asf_lightgrey.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/onboarding_asf_ic.png b/app/src/main/res/drawable-mdpi/onboarding_asf_ic.png deleted file mode 100644 index 263ed60f0af..00000000000 Binary files a/app/src/main/res/drawable-mdpi/onboarding_asf_ic.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/settings_rpc_server.png b/app/src/main/res/drawable-mdpi/settings_rpc_server.png deleted file mode 100644 index 477c5f52539..00000000000 Binary files a/app/src/main/res/drawable-mdpi/settings_rpc_server.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/token_icon.png b/app/src/main/res/drawable-mdpi/token_icon.png deleted file mode 100644 index 5558f0a7e9c..00000000000 Binary files a/app/src/main/res/drawable-mdpi/token_icon.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/no_transactions_mascot.png b/app/src/main/res/drawable-nodpi/no_transactions_mascot.png deleted file mode 100644 index 4628184d457..00000000000 Binary files a/app/src/main/res/drawable-nodpi/no_transactions_mascot.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/onboarding_erc20.png b/app/src/main/res/drawable-nodpi/onboarding_erc20.png deleted file mode 100644 index b843a48bcf9..00000000000 Binary files a/app/src/main/res/drawable-nodpi/onboarding_erc20.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/settings_facebook.png b/app/src/main/res/drawable-nodpi/settings_facebook.png deleted file mode 100644 index 12f5001e5d9..00000000000 Binary files a/app/src/main/res/drawable-nodpi/settings_facebook.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/check_mark.png b/app/src/main/res/drawable-xhdpi/check_mark.png deleted file mode 100644 index 41ff4420bdb..00000000000 Binary files a/app/src/main/res/drawable-xhdpi/check_mark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_deposit.png b/app/src/main/res/drawable-xhdpi/ic_action_deposit.png deleted file mode 100644 index a498ec114d5..00000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_action_deposit.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_description_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_description_black_24dp.png deleted file mode 100644 index 8470bc03fe5..00000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_description_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_facebook.png b/app/src/main/res/drawable-xhdpi/ic_facebook.png deleted file mode 100644 index a48f7fff333..00000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_facebook.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_refresh.png b/app/src/main/res/drawable-xhdpi/ic_refresh.png deleted file mode 100644 index eb0a1cc532f..00000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_refresh.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/icon_alpha_version.png b/app/src/main/res/drawable-xhdpi/icon_alpha_version.png deleted file mode 100644 index e3b5d40c871..00000000000 Binary files a/app/src/main/res/drawable-xhdpi/icon_alpha_version.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/icon_asf_lightgrey.png b/app/src/main/res/drawable-xhdpi/icon_asf_lightgrey.png deleted file mode 100644 index 9e8c516c166..00000000000 Binary files a/app/src/main/res/drawable-xhdpi/icon_asf_lightgrey.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/onboarding_asf_ic.png b/app/src/main/res/drawable-xhdpi/onboarding_asf_ic.png deleted file mode 100644 index 46dd977ff5d..00000000000 Binary files a/app/src/main/res/drawable-xhdpi/onboarding_asf_ic.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/settings_rpc_server.png b/app/src/main/res/drawable-xhdpi/settings_rpc_server.png deleted file mode 100644 index e79f387eeb4..00000000000 Binary files a/app/src/main/res/drawable-xhdpi/settings_rpc_server.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/token_icon.png b/app/src/main/res/drawable-xhdpi/token_icon.png deleted file mode 100644 index b4052ab18d8..00000000000 Binary files a/app/src/main/res/drawable-xhdpi/token_icon.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/check_mark.png b/app/src/main/res/drawable-xxhdpi/check_mark.png deleted file mode 100644 index e9a4a00ed27..00000000000 Binary files a/app/src/main/res/drawable-xxhdpi/check_mark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_deposit.png b/app/src/main/res/drawable-xxhdpi/ic_action_deposit.png deleted file mode 100644 index c3ca0ab8b4f..00000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_action_deposit.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_description_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_description_black_24dp.png deleted file mode 100644 index dd23b93ac24..00000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_description_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_facebook.png b/app/src/main/res/drawable-xxhdpi/ic_facebook.png deleted file mode 100644 index bc9983a5356..00000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_facebook.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_refresh.png b/app/src/main/res/drawable-xxhdpi/ic_refresh.png deleted file mode 100644 index c3bc32309cc..00000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_refresh.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/icon_alpha_version.png b/app/src/main/res/drawable-xxhdpi/icon_alpha_version.png deleted file mode 100644 index aba43da099f..00000000000 Binary files a/app/src/main/res/drawable-xxhdpi/icon_alpha_version.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/icon_asf_lightgrey.png b/app/src/main/res/drawable-xxhdpi/icon_asf_lightgrey.png deleted file mode 100644 index 63e3714b184..00000000000 Binary files a/app/src/main/res/drawable-xxhdpi/icon_asf_lightgrey.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/onboarding_asf_ic.png b/app/src/main/res/drawable-xxhdpi/onboarding_asf_ic.png deleted file mode 100644 index 11ac8f05131..00000000000 Binary files a/app/src/main/res/drawable-xxhdpi/onboarding_asf_ic.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/settings_rpc_server.png b/app/src/main/res/drawable-xxhdpi/settings_rpc_server.png deleted file mode 100644 index 6b3429c5515..00000000000 Binary files a/app/src/main/res/drawable-xxhdpi/settings_rpc_server.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/token_icon.png b/app/src/main/res/drawable-xxhdpi/token_icon.png deleted file mode 100644 index 1c3d705b2e2..00000000000 Binary files a/app/src/main/res/drawable-xxhdpi/token_icon.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/check_mark.png b/app/src/main/res/drawable-xxxhdpi/check_mark.png deleted file mode 100644 index 288a341f8c6..00000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/check_mark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_description_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_description_black_24dp.png deleted file mode 100644 index 687d5f85770..00000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_description_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/icon_alpha_version.png b/app/src/main/res/drawable-xxxhdpi/icon_alpha_version.png deleted file mode 100644 index bba3a3c067b..00000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/icon_alpha_version.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/icon_asf_lightgrey.png b/app/src/main/res/drawable-xxxhdpi/icon_asf_lightgrey.png deleted file mode 100644 index 2d41da37f97..00000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/icon_asf_lightgrey.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/onboarding_asf_ic.png b/app/src/main/res/drawable-xxxhdpi/onboarding_asf_ic.png deleted file mode 100644 index 357cfd30e97..00000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/onboarding_asf_ic.png and /dev/null differ diff --git a/app/src/main/res/drawable/appbar_background_color.xml b/app/src/main/res/drawable/appbar_background_color.xml index 8ea55498cd7..65e338553a9 100644 --- a/app/src/main/res/drawable/appbar_background_color.xml +++ b/app/src/main/res/drawable/appbar_background_color.xml @@ -2,8 +2,8 @@ diff --git a/app/src/main/res/drawable/arrow_rotated.xml b/app/src/main/res/drawable/arrow_rotated.xml new file mode 100644 index 00000000000..64fce61c446 --- /dev/null +++ b/app/src/main/res/drawable/arrow_rotated.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/authentication_bottomsheet_card.xml b/app/src/main/res/drawable/authentication_bottomsheet_card.xml new file mode 100644 index 00000000000..212aa9b99fe --- /dev/null +++ b/app/src/main/res/drawable/authentication_bottomsheet_card.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_card_bottom.xml b/app/src/main/res/drawable/background_card_bottom.xml new file mode 100644 index 00000000000..d9fdc66d317 --- /dev/null +++ b/app/src/main/res/drawable/background_card_bottom.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_card_grey.xml b/app/src/main/res/drawable/background_card_grey.xml new file mode 100644 index 00000000000..86a50784afe --- /dev/null +++ b/app/src/main/res/drawable/background_card_grey.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_card_top.xml b/app/src/main/res/drawable/background_card_top.xml new file mode 100644 index 00000000000..994f89d4d91 --- /dev/null +++ b/app/src/main/res/drawable/background_card_top.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_dark_grey.xml b/app/src/main/res/drawable/background_dark_grey.xml new file mode 100644 index 00000000000..792ac003ca5 --- /dev/null +++ b/app/src/main/res/drawable/background_dark_grey.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_fragment.xml b/app/src/main/res/drawable/background_fragment.xml new file mode 100644 index 00000000000..a01fb7762f2 --- /dev/null +++ b/app/src/main/res/drawable/background_fragment.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/background_translucent.xml b/app/src/main/res/drawable/background_translucent.xml new file mode 100644 index 00000000000..f22e9c2ebee --- /dev/null +++ b/app/src/main/res/drawable/background_translucent.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_white_card_top.xml b/app/src/main/res/drawable/background_white_card_top.xml new file mode 100644 index 00000000000..58ca20a7d08 --- /dev/null +++ b/app/src/main/res/drawable/background_white_card_top.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_ripple_primary_color.xml b/app/src/main/res/drawable/bg_ripple_primary_color.xml index 6487098cd37..269eeb8e3e5 100644 --- a/app/src/main/res/drawable/bg_ripple_primary_color.xml +++ b/app/src/main/res/drawable/bg_ripple_primary_color.xml @@ -1,13 +1,13 @@ - + - + \ No newline at end of file diff --git a/app/src/main/res/drawable/bonus_img_background.xml b/app/src/main/res/drawable/bonus_img_background.xml new file mode 100644 index 00000000000..917a037915f --- /dev/null +++ b/app/src/main/res/drawable/bonus_img_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/borderless_button_background.xml b/app/src/main/res/drawable/borderless_button_background.xml new file mode 100644 index 00000000000..7101dbb4585 --- /dev/null +++ b/app/src/main/res/drawable/borderless_button_background.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/borderless_button_ripple.xml b/app/src/main/res/drawable/borderless_button_ripple.xml new file mode 100644 index 00000000000..f47ab01c7c5 --- /dev/null +++ b/app/src/main/res/drawable/borderless_button_ripple.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bottom_sheet_background.xml b/app/src/main/res/drawable/bottom_sheet_background.xml new file mode 100644 index 00000000000..14867fe70cf --- /dev/null +++ b/app/src/main/res/drawable/bottom_sheet_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bottomsheet_card.xml b/app/src/main/res/drawable/bottomsheet_card.xml new file mode 100644 index 00000000000..04b6371fe6f --- /dev/null +++ b/app/src/main/res/drawable/bottomsheet_card.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bottomsheet_notch.xml b/app/src/main/res/drawable/bottomsheet_notch.xml new file mode 100644 index 00000000000..3d657ebb12d --- /dev/null +++ b/app/src/main/res/drawable/bottomsheet_notch.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bottomsheet_top_shadow.xml b/app/src/main/res/drawable/bottomsheet_top_shadow.xml new file mode 100644 index 00000000000..213fe7039d0 --- /dev/null +++ b/app/src/main/res/drawable/bottomsheet_top_shadow.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/btn_light_text_color.xml b/app/src/main/res/drawable/btn_light_text_color.xml new file mode 100644 index 00000000000..6b1d5542da4 --- /dev/null +++ b/app/src/main/res/drawable/btn_light_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_disable_snd_background.xml b/app/src/main/res/drawable/button_disable_snd_background.xml new file mode 100644 index 00000000000..fc23e7abae5 --- /dev/null +++ b/app/src/main/res/drawable/button_disable_snd_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_enabled_background.xml b/app/src/main/res/drawable/button_enabled_background.xml index 88a8bbf0f75..bd0e15a309f 100644 --- a/app/src/main/res/drawable/button_enabled_background.xml +++ b/app/src/main/res/drawable/button_enabled_background.xml @@ -1,19 +1,19 @@ - - - - - - - - - - - - + android:color="@color/btn_start_gradient_color"> + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_gradient_background.xml b/app/src/main/res/drawable/button_gradient_background.xml new file mode 100644 index 00000000000..f7c29511b5a --- /dev/null +++ b/app/src/main/res/drawable/button_gradient_background.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/drawable/button_light_background.xml b/app/src/main/res/drawable/button_light_background.xml new file mode 100644 index 00000000000..97e2720f5ce --- /dev/null +++ b/app/src/main/res/drawable/button_light_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_light_disabled_background.xml b/app/src/main/res/drawable/button_light_disabled_background.xml new file mode 100644 index 00000000000..5e0bcb61467 --- /dev/null +++ b/app/src/main/res/drawable/button_light_disabled_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_snd_background.xml b/app/src/main/res/drawable/button_snd_background.xml new file mode 100644 index 00000000000..ca4cbb2a5cc --- /dev/null +++ b/app/src/main/res/drawable/button_snd_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_background.xml b/app/src/main/res/drawable/circle_background.xml new file mode 100644 index 00000000000..a8cae3b61ba --- /dev/null +++ b/app/src/main/res/drawable/circle_background.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/clouds_background.xml b/app/src/main/res/drawable/clouds_background.xml new file mode 100644 index 00000000000..ac58d2d5634 --- /dev/null +++ b/app/src/main/res/drawable/clouds_background.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/app/src/main/res/drawable/dashed_line.xml b/app/src/main/res/drawable/dashed_line.xml new file mode 100644 index 00000000000..7d28bab2902 --- /dev/null +++ b/app/src/main/res/drawable/dashed_line.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dashed_vertical_line.xml b/app/src/main/res/drawable/dashed_vertical_line.xml new file mode 100644 index 00000000000..0681c9a5aea --- /dev/null +++ b/app/src/main/res/drawable/dashed_vertical_line.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/disable_bonus_img_background.xml b/app/src/main/res/drawable/disable_bonus_img_background.xml new file mode 100644 index 00000000000..d5bf218e76f --- /dev/null +++ b/app/src/main/res/drawable/disable_bonus_img_background.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/earned_value_background.xml b/app/src/main/res/drawable/earned_value_background.xml new file mode 100644 index 00000000000..93bfe751fd4 --- /dev/null +++ b/app/src/main/res/drawable/earned_value_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gamification_background.xml b/app/src/main/res/drawable/gamification_background.xml new file mode 100644 index 00000000000..f05b20cc605 --- /dev/null +++ b/app/src/main/res/drawable/gamification_background.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_earth.xml b/app/src/main/res/drawable/gamification_earth.xml new file mode 100644 index 00000000000..74496eeb77d --- /dev/null +++ b/app/src/main/res/drawable/gamification_earth.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/drawable/gamification_earth_reached.xml b/app/src/main/res/drawable/gamification_earth_reached.xml new file mode 100644 index 00000000000..b81d65dc39a --- /dev/null +++ b/app/src/main/res/drawable/gamification_earth_reached.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_europa.xml b/app/src/main/res/drawable/gamification_europa.xml new file mode 100644 index 00000000000..d4f4ffc1acb --- /dev/null +++ b/app/src/main/res/drawable/gamification_europa.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_europa_reached.xml b/app/src/main/res/drawable/gamification_europa_reached.xml new file mode 100644 index 00000000000..b779f2679e0 --- /dev/null +++ b/app/src/main/res/drawable/gamification_europa_reached.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_jupiter.xml b/app/src/main/res/drawable/gamification_jupiter.xml new file mode 100644 index 00000000000..a7c6c938233 --- /dev/null +++ b/app/src/main/res/drawable/gamification_jupiter.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_jupiter_reached.xml b/app/src/main/res/drawable/gamification_jupiter_reached.xml new file mode 100644 index 00000000000..242f30274a6 --- /dev/null +++ b/app/src/main/res/drawable/gamification_jupiter_reached.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_lock.xml b/app/src/main/res/drawable/gamification_lock.xml new file mode 100644 index 00000000000..f56a05028f1 --- /dev/null +++ b/app/src/main/res/drawable/gamification_lock.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/gamification_mars.xml b/app/src/main/res/drawable/gamification_mars.xml new file mode 100644 index 00000000000..c5e22f85399 --- /dev/null +++ b/app/src/main/res/drawable/gamification_mars.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_mars_reached.xml b/app/src/main/res/drawable/gamification_mars_reached.xml new file mode 100644 index 00000000000..a31e310bc2d --- /dev/null +++ b/app/src/main/res/drawable/gamification_mars_reached.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_moon.xml b/app/src/main/res/drawable/gamification_moon.xml new file mode 100644 index 00000000000..fc01be13c6d --- /dev/null +++ b/app/src/main/res/drawable/gamification_moon.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_moon_reached.xml b/app/src/main/res/drawable/gamification_moon_reached.xml new file mode 100644 index 00000000000..2e8ff0e8bf9 --- /dev/null +++ b/app/src/main/res/drawable/gamification_moon_reached.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_neptune.xml b/app/src/main/res/drawable/gamification_neptune.xml new file mode 100644 index 00000000000..d062fc90df2 --- /dev/null +++ b/app/src/main/res/drawable/gamification_neptune.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_neptune_reached.xml b/app/src/main/res/drawable/gamification_neptune_reached.xml new file mode 100644 index 00000000000..5be712697cd --- /dev/null +++ b/app/src/main/res/drawable/gamification_neptune_reached.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_phobos.xml b/app/src/main/res/drawable/gamification_phobos.xml new file mode 100644 index 00000000000..6d75c658aae --- /dev/null +++ b/app/src/main/res/drawable/gamification_phobos.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_phobos_reached.xml b/app/src/main/res/drawable/gamification_phobos_reached.xml new file mode 100644 index 00000000000..061aaf8675d --- /dev/null +++ b/app/src/main/res/drawable/gamification_phobos_reached.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_saturn.xml b/app/src/main/res/drawable/gamification_saturn.xml new file mode 100644 index 00000000000..6ccf2469f64 --- /dev/null +++ b/app/src/main/res/drawable/gamification_saturn.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_saturn_reached.xml b/app/src/main/res/drawable/gamification_saturn_reached.xml new file mode 100644 index 00000000000..ec0237f8076 --- /dev/null +++ b/app/src/main/res/drawable/gamification_saturn_reached.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_titan.xml b/app/src/main/res/drawable/gamification_titan.xml new file mode 100644 index 00000000000..61f1d5a2c79 --- /dev/null +++ b/app/src/main/res/drawable/gamification_titan.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_titan_reached.xml b/app/src/main/res/drawable/gamification_titan_reached.xml new file mode 100644 index 00000000000..03e9e9344c8 --- /dev/null +++ b/app/src/main/res/drawable/gamification_titan_reached.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_unknown_planet_blue.xml b/app/src/main/res/drawable/gamification_unknown_planet_blue.xml new file mode 100644 index 00000000000..486f43695af --- /dev/null +++ b/app/src/main/res/drawable/gamification_unknown_planet_blue.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_unknown_planet_blue_reached.xml b/app/src/main/res/drawable/gamification_unknown_planet_blue_reached.xml new file mode 100644 index 00000000000..9adc089b492 --- /dev/null +++ b/app/src/main/res/drawable/gamification_unknown_planet_blue_reached.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_unknown_planet_brown.xml b/app/src/main/res/drawable/gamification_unknown_planet_brown.xml new file mode 100644 index 00000000000..c59bad47a11 --- /dev/null +++ b/app/src/main/res/drawable/gamification_unknown_planet_brown.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_unknown_planet_brown_reached.xml b/app/src/main/res/drawable/gamification_unknown_planet_brown_reached.xml new file mode 100644 index 00000000000..ca6e293df0f --- /dev/null +++ b/app/src/main/res/drawable/gamification_unknown_planet_brown_reached.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_unknown_planet_green.xml b/app/src/main/res/drawable/gamification_unknown_planet_green.xml new file mode 100644 index 00000000000..3da01911c17 --- /dev/null +++ b/app/src/main/res/drawable/gamification_unknown_planet_green.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_unknown_planet_green_reached.xml b/app/src/main/res/drawable/gamification_unknown_planet_green_reached.xml new file mode 100644 index 00000000000..69b6c9f441a --- /dev/null +++ b/app/src/main/res/drawable/gamification_unknown_planet_green_reached.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_unknown_planet_purple.xml b/app/src/main/res/drawable/gamification_unknown_planet_purple.xml new file mode 100644 index 00000000000..eb10b327483 --- /dev/null +++ b/app/src/main/res/drawable/gamification_unknown_planet_purple.xml @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_unknown_planet_purple_reached.xml b/app/src/main/res/drawable/gamification_unknown_planet_purple_reached.xml new file mode 100644 index 00000000000..4cdf661d173 --- /dev/null +++ b/app/src/main/res/drawable/gamification_unknown_planet_purple_reached.xml @@ -0,0 +1,427 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_unknown_planet_red.xml b/app/src/main/res/drawable/gamification_unknown_planet_red.xml new file mode 100644 index 00000000000..a9ae17b4aca --- /dev/null +++ b/app/src/main/res/drawable/gamification_unknown_planet_red.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_unknown_planet_red_reached.xml b/app/src/main/res/drawable/gamification_unknown_planet_red_reached.xml new file mode 100644 index 00000000000..90da04d4391 --- /dev/null +++ b/app/src/main/res/drawable/gamification_unknown_planet_red_reached.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_uranus.xml b/app/src/main/res/drawable/gamification_uranus.xml new file mode 100644 index 00000000000..46c997f3f57 --- /dev/null +++ b/app/src/main/res/drawable/gamification_uranus.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gamification_uranus_reached.xml b/app/src/main/res/drawable/gamification_uranus_reached.xml new file mode 100644 index 00000000000..eff1bb3be10 --- /dev/null +++ b/app/src/main/res/drawable/gamification_uranus_reached.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/generic_progress_bar.xml b/app/src/main/res/drawable/generic_progress_bar.xml new file mode 100644 index 00000000000..b78ca1f261c --- /dev/null +++ b/app/src/main/res/drawable/generic_progress_bar.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ghost_button_ripple.xml b/app/src/main/res/drawable/ghost_button_ripple.xml new file mode 100644 index 00000000000..ef70afe4024 --- /dev/null +++ b/app/src/main/res/drawable/ghost_button_ripple.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gradient_progress.xml b/app/src/main/res/drawable/gradient_progress.xml index f11d5029e47..b39800d6ab1 100644 --- a/app/src/main/res/drawable/gradient_progress.xml +++ b/app/src/main/res/drawable/gradient_progress.xml @@ -7,16 +7,17 @@ - + + android:useLevel="false"/> \ No newline at end of file diff --git a/app/src/main/res/drawable/iab_completed_graphic.xml b/app/src/main/res/drawable/iab_completed_graphic.xml deleted file mode 100644 index e3d0f925fe3..00000000000 --- a/app/src/main/res/drawable/iab_completed_graphic.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_circle_black_24dp.xml b/app/src/main/res/drawable/ic_add_circle_black_24dp.xml deleted file mode 100644 index ac84965db49..00000000000 --- a/app/src/main/res/drawable/ic_add_circle_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_app_logo.xml b/app/src/main/res/drawable/ic_app_logo.xml index 878442b86df..2a80e6383ec 100644 --- a/app/src/main/res/drawable/ic_app_logo.xml +++ b/app/src/main/res/drawable/ic_app_logo.xml @@ -1,20 +1,34 @@ + android:pathData="M11.991,0C7.121,0 2.924,2.905 1.058,7.074A11.918,11.918 0,0 0,0 12.004C0,18.63 5.372,24 12,24s12,-5.37 12,-11.996c0,-1.756 -0.377,-3.424 -1.058,-4.93C21.058,2.905 16.86,0 11.99,0z" + android:fillType="evenOdd"> + + + + + + + + android:pathData="M10.46,12.878l1.558,-4.744 1.587,4.744L10.46,12.878zM15.778,12.335h0.804a0.534,0.534 0,0 0,0.534 -0.543,0.553 0.553,0 0,0 -0.553,-0.543L15.38,11.249l-0.213,-0.55h1.38a0.543,0.543 0,0 0,0 -1.086h-1.779l-0.972,-2.65c-0.148,-0.357 -0.367,-0.721 -0.691,-0.973 -0.29,-0.26 -0.685,-0.365 -1.087,-0.365 -0.402,0 -0.804,0.112 -1.093,0.365a2.412,2.412 0,0 0,-0.69 0.974l-1,2.663L7.46,9.627a0.531,0.531 0,0 0,-0.53 0.536,0.545 0.545,0 0,0 0.54,0.54l1.359,0.01 -0.207,0.536L7.407,11.249a0.53,0.53 0,0 0,-0.53 0.53,0.54 0.54,0 0,0 0.53,0.54l0.807,0.016 -1.323,3.543c-0.078,0.147 -0.113,0.364 -0.141,0.63 0,0.288 0.148,0.575 0.367,0.792 0.254,0.217 0.55,0.323 0.874,0.323a1.18,1.18 0,0 0,1.2 -0.87l0.507,-1.513h4.683l0.508,1.549c0.148,0.504 0.656,0.862 1.199,0.827 0.211,0 0.395,-0.043 0.578,-0.148a1.97,1.97 0,0 0,0.402 -0.399,1.08 1.08,0 0,0 0.148,-0.575c-0.042,-0.217 -0.077,-0.434 -0.148,-0.616l-1.29,-3.543z" + android:fillColor="#FEFEFE" + android:fillType="evenOdd"/> + diff --git a/app/src/main/res/drawable/ic_appc.xml b/app/src/main/res/drawable/ic_appc.xml new file mode 100644 index 00000000000..f0fc5b2b9bf --- /dev/null +++ b/app/src/main/res/drawable/ic_appc.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_appc_c_token.xml b/app/src/main/res/drawable/ic_appc_c_token.xml new file mode 100644 index 00000000000..9c4fc9dff33 --- /dev/null +++ b/app/src/main/res/drawable/ic_appc_c_token.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_appc_token.xml b/app/src/main/res/drawable/ic_appc_token.xml new file mode 100644 index 00000000000..643ff5a8130 --- /dev/null +++ b/app/src/main/res/drawable/ic_appc_token.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_down.xml b/app/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 00000000000..60d99620c53 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_downward_black_24dp.xml b/app/src/main/res/drawable/ic_arrow_downward_black_24dp.xml deleted file mode 100644 index 59dfb2aa77f..00000000000 --- a/app/src/main/res/drawable/ic_arrow_downward_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_right.xml b/app/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 00000000000..7c74376eaa8 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_up.xml b/app/src/main/res/drawable/ic_arrow_up.xml new file mode 100644 index 00000000000..36cccd085a1 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_upward_black_24dp.xml b/app/src/main/res/drawable/ic_arrow_upward_black_24dp.xml deleted file mode 100644 index 78c6e7a2f24..00000000000 --- a/app/src/main/res/drawable/ic_arrow_upward_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_astronaut.xml b/app/src/main/res/drawable/ic_astronaut.xml new file mode 100644 index 00000000000..fa0eba79fde --- /dev/null +++ b/app/src/main/res/drawable/ic_astronaut.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_attach_money_white_24dp.xml b/app/src/main/res/drawable/ic_attach_money_white_24dp.xml deleted file mode 100644 index 97626c9abfd..00000000000 --- a/app/src/main/res/drawable/ic_attach_money_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_backup_black.xml b/app/src/main/res/drawable/ic_backup_black.xml new file mode 100644 index 00000000000..9042361730f --- /dev/null +++ b/app/src/main/res/drawable/ic_backup_black.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_backup_black_24dp.xml b/app/src/main/res/drawable/ic_backup_black_24dp.xml deleted file mode 100644 index 4a1157fe72d..00000000000 --- a/app/src/main/res/drawable/ic_backup_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_backup_confirm.xml b/app/src/main/res/drawable/ic_backup_confirm.xml new file mode 100644 index 00000000000..ddb60e3a13a --- /dev/null +++ b/app/src/main/res/drawable/ic_backup_confirm.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_backup_notification.xml b/app/src/main/res/drawable/ic_backup_notification.xml new file mode 100644 index 00000000000..2328a3d5c5a --- /dev/null +++ b/app/src/main/res/drawable/ic_backup_notification.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_backup_wallet.xml b/app/src/main/res/drawable/ic_backup_wallet.xml new file mode 100644 index 00000000000..e2c8c420cb1 --- /dev/null +++ b/app/src/main/res/drawable/ic_backup_wallet.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_backup_white.xml b/app/src/main/res/drawable/ic_backup_white.xml new file mode 100644 index 00000000000..e4e345eb3dc --- /dev/null +++ b/app/src/main/res/drawable/ic_backup_white.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_bonus.xml b/app/src/main/res/drawable/ic_bonus.xml new file mode 100644 index 00000000000..333fa9d1a01 --- /dev/null +++ b/app/src/main/res/drawable/ic_bonus.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_bonus_pending.xml b/app/src/main/res/drawable/ic_bonus_pending.xml new file mode 100644 index 00000000000..852b6421336 --- /dev/null +++ b/app/src/main/res/drawable/ic_bonus_pending.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_bonus_updated.xml b/app/src/main/res/drawable/ic_bonus_updated.xml new file mode 100644 index 00000000000..a486e1e03e8 --- /dev/null +++ b/app/src/main/res/drawable/ic_bonus_updated.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_bottom_nav_balance.xml b/app/src/main/res/drawable/ic_bottom_nav_balance.xml deleted file mode 100644 index f9e88fb0093..00000000000 --- a/app/src/main/res/drawable/ic_bottom_nav_balance.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_call_received_white_24dp.xml b/app/src/main/res/drawable/ic_call_received_white_24dp.xml deleted file mode 100644 index d668646b4aa..00000000000 --- a/app/src/main/res/drawable/ic_call_received_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_check_for_update.xml b/app/src/main/res/drawable/ic_check_for_update.xml new file mode 100644 index 00000000000..2b04c2769f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_for_update.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_right_black_24dp.xml b/app/src/main/res/drawable/ic_chevron_right_black_24dp.xml deleted file mode 100644 index aa121a8cc07..00000000000 --- a/app/src/main/res/drawable/ic_chevron_right_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_clock.xml b/app/src/main/res/drawable/ic_clock.xml new file mode 100644 index 00000000000..acc56d39cbc --- /dev/null +++ b/app/src/main/res/drawable/ic_clock.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/ic_code_black_24dp.xml b/app/src/main/res/drawable/ic_code_black_24dp.xml deleted file mode 100644 index 4cf5096d1fe..00000000000 --- a/app/src/main/res/drawable/ic_code_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_content_paste_black_24dp.xml b/app/src/main/res/drawable/ic_content_paste_black_24dp.xml deleted file mode 100644 index 962ef8d6b49..00000000000 --- a/app/src/main/res/drawable/ic_content_paste_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_copy_to_clip.xml b/app/src/main/res/drawable/ic_copy_to_clip.xml new file mode 100644 index 00000000000..d60f3e587f5 --- /dev/null +++ b/app/src/main/res/drawable/ic_copy_to_clip.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_copy_to_clip_pink.xml b/app/src/main/res/drawable/ic_copy_to_clip_pink.xml new file mode 100644 index 00000000000..b39fae34f62 --- /dev/null +++ b/app/src/main/res/drawable/ic_copy_to_clip_pink.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_create_new_wallet.xml b/app/src/main/res/drawable/ic_create_new_wallet.xml new file mode 100644 index 00000000000..ac566926295 --- /dev/null +++ b/app/src/main/res/drawable/ic_create_new_wallet.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_dashboard_black_24dp.xml b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml deleted file mode 100644 index 214631412f0..00000000000 --- a/app/src/main/res/drawable/ic_dashboard_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_edit_white_24dp.xml b/app/src/main/res/drawable/ic_edit_white_24dp.xml deleted file mode 100644 index 70727b26179..00000000000 --- a/app/src/main/res/drawable/ic_edit_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_error_outline_black_24dp.xml b/app/src/main/res/drawable/ic_error_outline_black_24dp.xml deleted file mode 100644 index 95f3c3aa05c..00000000000 --- a/app/src/main/res/drawable/ic_error_outline_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_eth_token.xml b/app/src/main/res/drawable/ic_eth_token.xml new file mode 100644 index 00000000000..7256874bdfd --- /dev/null +++ b/app/src/main/res/drawable/ic_eth_token.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_gamification_level_stats.xml b/app/src/main/res/drawable/ic_gamification_level_stats.xml new file mode 100644 index 00000000000..3487146c68f --- /dev/null +++ b/app/src/main/res/drawable/ic_gamification_level_stats.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_home_black_24dp.xml b/app/src/main/res/drawable/ic_home_black_24dp.xml deleted file mode 100644 index b6c98ae7c27..00000000000 --- a/app/src/main/res/drawable/ic_home_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 00000000000..447d14048cd --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_info_grey.xml b/app/src/main/res/drawable/ic_info_grey.xml new file mode 100644 index 00000000000..fc7bb2d27e0 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_grey.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_info_white_bg.xml b/app/src/main/res/drawable/ic_info_white_bg.xml new file mode 100644 index 00000000000..ff99a501f3e --- /dev/null +++ b/app/src/main/res/drawable/ic_info_white_bg.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_issue_report.xml b/app/src/main/res/drawable/ic_issue_report.xml new file mode 100644 index 00000000000..fda8340b4c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_issue_report.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000000..2c8e1161fa3 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 388139c5aa0..d17bcda07c9 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,51 +1,18 @@ - - - - - - - - - - - - - - - - - - + + + diff --git a/app/src/main/res/drawable/ic_logo_appc_support.xml b/app/src/main/res/drawable/ic_logo_appc_support.xml new file mode 100644 index 00000000000..28ad5da737e --- /dev/null +++ b/app/src/main/res/drawable/ic_logo_appc_support.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_new_option.xml b/app/src/main/res/drawable/ic_new_option.xml new file mode 100644 index 00000000000..f64c8e6e44b --- /dev/null +++ b/app/src/main/res/drawable/ic_new_option.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 00000000000..09154ace49a --- /dev/null +++ b/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml deleted file mode 100644 index 150a5821dbe..00000000000 --- a/app/src/main/res/drawable/ic_notifications_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_open_in_new_24.xml b/app/src/main/res/drawable/ic_open_in_new_24.xml new file mode 100644 index 00000000000..8edced9df52 --- /dev/null +++ b/app/src/main/res/drawable/ic_open_in_new_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_password_locket.xml b/app/src/main/res/drawable/ic_password_locket.xml new file mode 100644 index 00000000000..2efbf5b997e --- /dev/null +++ b/app/src/main/res/drawable/ic_password_locket.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_payment_method_credit_card.xml b/app/src/main/res/drawable/ic_payment_method_credit_card.xml new file mode 100644 index 00000000000..a0f88a3141b --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_method_credit_card.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_payment_method_paypal.xml b/app/src/main/res/drawable/ic_payment_method_paypal.xml new file mode 100644 index 00000000000..87e52531ca0 --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_method_paypal.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_promotions.xml b/app/src/main/res/drawable/ic_promotions.xml new file mode 100644 index 00000000000..7f5f3caf4a7 --- /dev/null +++ b/app/src/main/res/drawable/ic_promotions.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_promotions_default.xml b/app/src/main/res/drawable/ic_promotions_default.xml new file mode 100644 index 00000000000..ab557b00502 --- /dev/null +++ b/app/src/main/res/drawable/ic_promotions_default.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_promotions_lock.xml b/app/src/main/res/drawable/ic_promotions_lock.xml new file mode 100644 index 00000000000..8ea73640243 --- /dev/null +++ b/app/src/main/res/drawable/ic_promotions_lock.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_qr_code.xml b/app/src/main/res/drawable/ic_qr_code.xml new file mode 100644 index 00000000000..9030ab18a9f --- /dev/null +++ b/app/src/main/res/drawable/ic_qr_code.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_referral_phone_verification.xml b/app/src/main/res/drawable/ic_referral_phone_verification.xml new file mode 100644 index 00000000000..cd976e0d7e5 --- /dev/null +++ b/app/src/main/res/drawable/ic_referral_phone_verification.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_referral_shop_cart.xml b/app/src/main/res/drawable/ic_referral_shop_cart.xml new file mode 100644 index 00000000000..396eb1dadea --- /dev/null +++ b/app/src/main/res/drawable/ic_referral_shop_cart.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_remove_wallet.xml b/app/src/main/res/drawable/ic_remove_wallet.xml new file mode 100644 index 00000000000..4a378f5ef47 --- /dev/null +++ b/app/src/main/res/drawable/ic_remove_wallet.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_restore.xml b/app/src/main/res/drawable/ic_restore.xml new file mode 100644 index 00000000000..22913a7a1f1 --- /dev/null +++ b/app/src/main/res/drawable/ic_restore.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_restore_black.xml b/app/src/main/res/drawable/ic_restore_black.xml new file mode 100644 index 00000000000..4f9d0a4b147 --- /dev/null +++ b/app/src/main/res/drawable/ic_restore_black.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_restore_wallet.xml b/app/src/main/res/drawable/ic_restore_wallet.xml new file mode 100644 index 00000000000..7f09c329951 --- /dev/null +++ b/app/src/main/res/drawable/ic_restore_wallet.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_send_white_24dp.xml b/app/src/main/res/drawable/ic_send_white_24dp.xml deleted file mode 100644 index f31cb08fce5..00000000000 --- a/app/src/main/res/drawable/ic_send_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_settings_credits.xml b/app/src/main/res/drawable/ic_settings_credits.xml new file mode 100644 index 00000000000..b513fc4f180 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_credits.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_email.xml b/app/src/main/res/drawable/ic_settings_email.xml new file mode 100644 index 00000000000..2bf917e7953 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_email.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_fb.xml b/app/src/main/res/drawable/ic_settings_fb.xml new file mode 100644 index 00000000000..a16f22b8492 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_fb.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_fingerprint.xml b/app/src/main/res/drawable/ic_settings_fingerprint.xml new file mode 100644 index 00000000000..0bb88fb3ec8 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_fingerprint.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_permissions.xml b/app/src/main/res/drawable/ic_settings_permissions.xml new file mode 100644 index 00000000000..70244e95797 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_permissions.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_privacypolicy.xml b/app/src/main/res/drawable/ic_settings_privacypolicy.xml new file mode 100644 index 00000000000..cf4b429964e --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_privacypolicy.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_redeem.xml b/app/src/main/res/drawable/ic_settings_redeem.xml new file mode 100644 index 00000000000..b6b03eb6a88 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_redeem.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_sourcecode.xml b/app/src/main/res/drawable/ic_settings_sourcecode.xml new file mode 100644 index 00000000000..77cddaf7af7 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_sourcecode.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_support.xml b/app/src/main/res/drawable/ic_settings_support.xml new file mode 100644 index 00000000000..47ddf700c73 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_support.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_telegram.xml b/app/src/main/res/drawable/ic_settings_telegram.xml new file mode 100644 index 00000000000..731bcbb0b90 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_telegram.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_termsconditions.xml b/app/src/main/res/drawable/ic_settings_termsconditions.xml new file mode 100644 index 00000000000..703959ae27f --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_termsconditions.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_twt.xml b/app/src/main/res/drawable/ic_settings_twt.xml new file mode 100644 index 00000000000..8add280a49f --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_twt.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_verification.xml b/app/src/main/res/drawable/ic_settings_verification.xml new file mode 100644 index 00000000000..9e7960a74d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_verification.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_verification_disabled.xml b/app/src/main/res/drawable/ic_settings_verification_disabled.xml new file mode 100644 index 00000000000..8780bea4bdf --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_verification_disabled.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_version.xml b/app/src/main/res/drawable/ic_settings_version.xml new file mode 100644 index 00000000000..058d33ebe57 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_version.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_wallets.xml b/app/src/main/res/drawable/ic_settings_wallets.xml new file mode 100644 index 00000000000..0dcec37b7b1 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_wallets.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_share_black.xml b/app/src/main/res/drawable/ic_share_black.xml new file mode 100644 index 00000000000..2a763e3abe6 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_black.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_share_black_24dp.xml b/app/src/main/res/drawable/ic_share_black_24dp.xml deleted file mode 100644 index b44cb8a1b2b..00000000000 --- a/app/src/main/res/drawable/ic_share_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_share_disabled.xml b/app/src/main/res/drawable/ic_share_disabled.xml new file mode 100644 index 00000000000..b27acd8c6e0 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_disabled.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_enabled.xml b/app/src/main/res/drawable/ic_share_enabled.xml new file mode 100644 index 00000000000..71a56f61d10 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_enabled.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_selector.xml b/app/src/main/res/drawable/ic_share_selector.xml new file mode 100644 index 00000000000..00ff9191afd --- /dev/null +++ b/app/src/main/res/drawable/ic_share_selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_share_white.xml b/app/src/main/res/drawable/ic_share_white.xml new file mode 100644 index 00000000000..9eb1848c525 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_white.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_slim_arrow_down.xml b/app/src/main/res/drawable/ic_slim_arrow_down.xml new file mode 100644 index 00000000000..e0dc65d32d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_slim_arrow_down.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_slim_arrow_up.xml b/app/src/main/res/drawable/ic_slim_arrow_up.xml new file mode 100644 index 00000000000..2c084e8333a --- /dev/null +++ b/app/src/main/res/drawable/ic_slim_arrow_up.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_yellow_24dp.xml b/app/src/main/res/drawable/ic_star_yellow_24dp.xml new file mode 100644 index 00000000000..a295b0f8276 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_yellow_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_swap.xml b/app/src/main/res/drawable/ic_swap.xml new file mode 100644 index 00000000000..f49231ad883 --- /dev/null +++ b/app/src/main/res/drawable/ic_swap.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_swap_horiz_white_24dp.xml b/app/src/main/res/drawable/ic_swap_horiz_white_24dp.xml deleted file mode 100644 index 3ea8d9f3b1c..00000000000 --- a/app/src/main/res/drawable/ic_swap_horiz_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_topup_error.xml b/app/src/main/res/drawable/ic_topup_error.xml new file mode 100644 index 00000000000..352b045576c --- /dev/null +++ b/app/src/main/res/drawable/ic_topup_error.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_transaction_iab.xml b/app/src/main/res/drawable/ic_transaction_iab.xml index 728f61f2bbd..714d3e38e2e 100644 --- a/app/src/main/res/drawable/ic_transaction_iab.xml +++ b/app/src/main/res/drawable/ic_transaction_iab.xml @@ -9,13 +9,13 @@ android:fillType="nonZero"> - - + android:type="linear" + android:endX="21.026594"> + + diff --git a/app/src/main/res/drawable/ic_transaction_peer.xml b/app/src/main/res/drawable/ic_transaction_peer.xml index dbdd265b16a..0418e3621e0 100644 --- a/app/src/main/res/drawable/ic_transaction_peer.xml +++ b/app/src/main/res/drawable/ic_transaction_peer.xml @@ -9,13 +9,13 @@ android:fillType="nonZero"> - - + android:type="linear" + android:endX="20"> + + diff --git a/app/src/main/res/drawable/ic_transaction_poa.xml b/app/src/main/res/drawable/ic_transaction_poa.xml index 6ffefd19e7e..3cc2891aeee 100644 --- a/app/src/main/res/drawable/ic_transaction_poa.xml +++ b/app/src/main/res/drawable/ic_transaction_poa.xml @@ -9,13 +9,13 @@ android:fillType="nonZero"> - - + android:type="linear" + android:endX="23"> + + diff --git a/app/src/main/res/drawable/ic_transfer_edit.xml b/app/src/main/res/drawable/ic_transfer_edit.xml new file mode 100644 index 00000000000..e9466d1faa6 --- /dev/null +++ b/app/src/main/res/drawable/ic_transfer_edit.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_up_arrow.xml b/app/src/main/res/drawable/ic_up_arrow.xml new file mode 100644 index 00000000000..a100aa860e8 --- /dev/null +++ b/app/src/main/res/drawable/ic_up_arrow.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_wallet_address.xml b/app/src/main/res/drawable/ic_wallet_address.xml new file mode 100644 index 00000000000..394735a31fb --- /dev/null +++ b/app/src/main/res/drawable/ic_wallet_address.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_wallet_address_secondary.xml b/app/src/main/res/drawable/ic_wallet_address_secondary.xml new file mode 100644 index 00000000000..1415bc90335 --- /dev/null +++ b/app/src/main/res/drawable/ic_wallet_address_secondary.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_wallet_black.xml b/app/src/main/res/drawable/ic_wallet_black.xml new file mode 100644 index 00000000000..6ebf2409b5c --- /dev/null +++ b/app/src/main/res/drawable/ic_wallet_black.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_wallet_unverified_chip.xml b/app/src/main/res/drawable/ic_wallet_unverified_chip.xml new file mode 100644 index 00000000000..e45096b2c90 --- /dev/null +++ b/app/src/main/res/drawable/ic_wallet_unverified_chip.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_wallet_verified_chip.xml b/app/src/main/res/drawable/ic_wallet_verified_chip.xml new file mode 100644 index 00000000000..20ce69f492f --- /dev/null +++ b/app/src/main/res/drawable/ic_wallet_verified_chip.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/img_permissions_empty_state.xml b/app/src/main/res/drawable/img_permissions_empty_state.xml new file mode 100644 index 00000000000..42446e946da --- /dev/null +++ b/app/src/main/res/drawable/img_permissions_empty_state.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/item_curved_border.xml b/app/src/main/res/drawable/item_curved_border.xml new file mode 100644 index 00000000000..eb2f402cea0 --- /dev/null +++ b/app/src/main/res/drawable/item_curved_border.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/level_icon_background.xml b/app/src/main/res/drawable/level_icon_background.xml new file mode 100644 index 00000000000..48cd5128519 --- /dev/null +++ b/app/src/main/res/drawable/level_icon_background.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/level_icon_background_border.xml b/app/src/main/res/drawable/level_icon_background_border.xml new file mode 100644 index 00000000000..229320b72a8 --- /dev/null +++ b/app/src/main/res/drawable/level_icon_background_border.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/logo_appc_wallet.xml b/app/src/main/res/drawable/logo_appc_wallet.xml new file mode 100644 index 00000000000..9a526e660ca --- /dev/null +++ b/app/src/main/res/drawable/logo_appc_wallet.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/notification_badge_circle.xml b/app/src/main/res/drawable/notification_badge_circle.xml new file mode 100644 index 00000000000..ad8ad152fcf --- /dev/null +++ b/app/src/main/res/drawable/notification_badge_circle.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/oval_green_background.xml b/app/src/main/res/drawable/oval_green_background.xml new file mode 100644 index 00000000000..a96464a3f35 --- /dev/null +++ b/app/src/main/res/drawable/oval_green_background.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/oval_grey_background.xml b/app/src/main/res/drawable/oval_grey_background.xml new file mode 100644 index 00000000000..9daf24d40a1 --- /dev/null +++ b/app/src/main/res/drawable/oval_grey_background.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/oval_toggle_button.xml b/app/src/main/res/drawable/oval_toggle_button.xml new file mode 100644 index 00000000000..51d1488c4ad --- /dev/null +++ b/app/src/main/res/drawable/oval_toggle_button.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/payment_method_background.xml b/app/src/main/res/drawable/payment_method_background.xml new file mode 100644 index 00000000000..11ca9f2a7d3 --- /dev/null +++ b/app/src/main/res/drawable/payment_method_background.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_bar_gradient.xml b/app/src/main/res/drawable/progress_bar_gradient.xml new file mode 100644 index 00000000000..38bdd640bdd --- /dev/null +++ b/app/src/main/res/drawable/progress_bar_gradient.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/promotions_date_background.xml b/app/src/main/res/drawable/promotions_date_background.xml new file mode 100644 index 00000000000..fc200de3083 --- /dev/null +++ b/app/src/main/res/drawable/promotions_date_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/promotions_future_background.xml b/app/src/main/res/drawable/promotions_future_background.xml new file mode 100644 index 00000000000..5892c1ce757 --- /dev/null +++ b/app/src/main/res/drawable/promotions_future_background.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/promotions_progress_bar.xml b/app/src/main/res/drawable/promotions_progress_bar.xml new file mode 100644 index 00000000000..5943e8fa94a --- /dev/null +++ b/app/src/main/res/drawable/promotions_progress_bar.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rectangle_outline_grey.xml b/app/src/main/res/drawable/rectangle_outline_grey.xml new file mode 100644 index 00000000000..a082a26756b --- /dev/null +++ b/app/src/main/res/drawable/rectangle_outline_grey.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rectangle_outline_grey_radius_8dp.xml b/app/src/main/res/drawable/rectangle_outline_grey_radius_8dp.xml new file mode 100644 index 00000000000..ee1a9c91a1a --- /dev/null +++ b/app/src/main/res/drawable/rectangle_outline_grey_radius_8dp.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rectangle_outline_red.xml b/app/src/main/res/drawable/rectangle_outline_red.xml new file mode 100644 index 00000000000..2a2ba3e8d43 --- /dev/null +++ b/app/src/main/res/drawable/rectangle_outline_red.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rectangle_solid_white.xml b/app/src/main/res/drawable/rectangle_solid_white.xml new file mode 100644 index 00000000000..3a5da538b44 --- /dev/null +++ b/app/src/main/res/drawable/rectangle_solid_white.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/referral_invitation.xml b/app/src/main/res/drawable/referral_invitation.xml new file mode 100644 index 00000000000..38b6fb86b0c --- /dev/null +++ b/app/src/main/res/drawable/referral_invitation.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/referrals_background.xml b/app/src/main/res/drawable/referrals_background.xml new file mode 100644 index 00000000000..3bb7a59cdb7 --- /dev/null +++ b/app/src/main/res/drawable/referrals_background.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/round_thumb.xml b/app/src/main/res/drawable/round_thumb.xml new file mode 100644 index 00000000000..06180f27886 --- /dev/null +++ b/app/src/main/res/drawable/round_thumb.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/round_track.xml b/app/src/main/res/drawable/round_track.xml new file mode 100644 index 00000000000..a796eb15e04 --- /dev/null +++ b/app/src/main/res/drawable/round_track.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/skeleton_circle.xml b/app/src/main/res/drawable/skeleton_circle.xml new file mode 100644 index 00000000000..d1b81c8d7f1 --- /dev/null +++ b/app/src/main/res/drawable/skeleton_circle.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/skeleton_row.xml b/app/src/main/res/drawable/skeleton_row.xml new file mode 100644 index 00000000000..c7bb11bf166 --- /dev/null +++ b/app/src/main/res/drawable/skeleton_row.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/skeleton_square.xml b/app/src/main/res/drawable/skeleton_square.xml new file mode 100644 index 00000000000..ff0e8ab0ae8 --- /dev/null +++ b/app/src/main/res/drawable/skeleton_square.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/space_background_card.xml b/app/src/main/res/drawable/space_background_card.xml new file mode 100644 index 00000000000..faabdc2bcd2 --- /dev/null +++ b/app/src/main/res/drawable/space_background_card.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/splash_background.xml b/app/src/main/res/drawable/splash_background.xml index 78cce957fce..0abb8252839 100644 --- a/app/src/main/res/drawable/splash_background.xml +++ b/app/src/main/res/drawable/splash_background.xml @@ -1,7 +1,7 @@ - + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/transaction_type_top_up.xml b/app/src/main/res/drawable/transaction_type_top_up.xml new file mode 100644 index 00000000000..673d55ff56e --- /dev/null +++ b/app/src/main/res/drawable/transaction_type_top_up.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/drawable/transaction_type_transfer_off_chain.xml b/app/src/main/res/drawable/transaction_type_transfer_off_chain.xml new file mode 100644 index 00000000000..5988d337f23 --- /dev/null +++ b/app/src/main/res/drawable/transaction_type_transfer_off_chain.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/app/src/main/res/drawable/transactions_promotion_bonus.xml b/app/src/main/res/drawable/transactions_promotion_bonus.xml new file mode 100644 index 00000000000..c77c37bf89d --- /dev/null +++ b/app/src/main/res/drawable/transactions_promotion_bonus.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/external_strings/values-ar/external_strings.xml b/app/src/main/res/external_strings/values-ar/external_strings.xml new file mode 100644 index 00000000000..6ff4d8fc531 --- /dev/null +++ b/app/src/main/res/external_strings/values-ar/external_strings.xml @@ -0,0 +1,58 @@ + + + + لشراء هذا العنصر أنت بحاجة أولًا للحصول على %s. + AppCoins Wallet + إغلاق + + أنت بحاجة إلى تطبيق AppCoins Wallet لإتمام الشراء. يمكنك تنزيله من Aptoide أو Play Store والعودة لإتمام الشراء! + مفهوم! + + أنت بحاجة إلى محفظة AppCoins Wallet! + للحصول على المكافأة أنت بحاجة إلى محفظة AppCoins Wallet. + + ادفع كزائر + بطاقة ائتمان + بايبال + ادفع باستخدام بايبال + ادفع باستخدام بطاقة ائتمان + رقم البطاقة + سنة/شهر + رمز التحقق + رمز التحقق + تغيير البطاقة + ادفع باستخدام محفظة AppCoins + احصل على مكافأة حتى %s%%! + ستحصل على %s على عملية الشراء هذه. + ستتلقى مكافأة على كل عملية شراء. + أفضل صفقة + انتهينا! + في المرة القادمة، احصل على مكافأة تصل إلى %s%% مع محفظة AppCoins Wallet! + في المرة القادمة، احصل على مكافأة مع محفظة Appcoins Wallet! + كان بإمكانك الحصول على %s على عملية الشراء هذه. + انتهت عملية الشراء! + وسائل دفع أخرى + تحتاج مساعدة؟ + تواصل مع الدعم. + + التالي + إلغاء + شراء + تثبيت المحفظة + موافق + تثبيت + + خطأ + أنت تملك هذا العنصر بالفعل! + ياه، حدث خطأ. + هناك مشكلة ببطاقتك. يرجى المحاولة مجددًا أو التواصل معنا. + تم رفض التحويل من قبل البنك الخاص بك. يرجى المحاولة ببطاقة أخرى أو التواصل معنا. + يبدو أنك لا تملك رصيدًا كافيًا أو أن هناك حدًا على بطاقتك. يرجى المحاولة ببطاقة أخرى. + يبدو أن بطاقتك قد انتهت صلاحيتها. يرجى المحاولة ببطاقة أخرى. + هل أنت متأكد من أن رقم بطاقتك صحيح؟ يرجى التحقق والمحاولة مجددًا. + إن نوع بطاقتك غير مدعوم حاليًا. حاول مع بطاقة أخرى. + هل أنت متأكد من أن المعلومات الأمنية صحيحة؟ يرجى المحاولة ثانية. + يبدو أن رمز التحقق خطأ. يرجى المحاولة ثانية. + رمز تحقق خاطئ + إذا استمرت هذه المشكلة، يرجى التواصل معنا. + diff --git a/app/src/main/res/external_strings/values-ar/perks.po b/app/src/main/res/external_strings/values-ar/perks.po new file mode 100644 index 00000000000..123a8a1bc4e --- /dev/null +++ b/app/src/main/res/external_strings/values-ar/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: ar\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Arabic\n" +"Language: ar_SA\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "اربح رصيد AppCoins!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "قم بعملية شراء في {0} واحصل على رصيد {1} Appcoins." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "لأنك قمت بالشراء في {0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "أنفق {0} {1} في {2} أيام واحصل على رصيد {3} AppCoins." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "لأنك أنفقت أكثر من {0} {1} في أخر {2} يوم." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "انتقل إلى المستوى التالي واحصل على رصيد {0} AppCoins!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "لأنك وصلت إلى المستوى التالي." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "احصل على مكافأة {0}% إضافية في كل عمليات الشراء، ما عدا رصيد AppCoins، في لعبة {1}." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "احصل على مكافأة {0}% إضافية في أول عملية شراء لك، ما عدا رصيد AppCoins، في لعبة {1}" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "لأنك اشتريت لأول مرة في {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "احصل على مكافأة {0}% إضافية في أول عملية شراء في اليوم، ما عدا رصيد AppCoins، في {1}" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "لأنك اشتريت لأول مرة في اليوم في {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} رصيد AppCoins مكافأة!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "لأنك أنفقت {0} {1} في {2}." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-bg/external_strings.xml b/app/src/main/res/external_strings/values-bg/external_strings.xml new file mode 100644 index 00000000000..5c723f74eaf --- /dev/null +++ b/app/src/main/res/external_strings/values-bg/external_strings.xml @@ -0,0 +1,58 @@ + + + + За да купиш този артикул, първо трябва да вземеш %s. + AppCoins Wallet + ЗАТВОРИ + + Имаш нужда от AppCoins Wallet, за да направиш тази покупка. Изтегли го от Aptoide или Play Store и се върни, за да завършиш покупката си! + РАЗБРАХ! + + Имаш нужда от AppCoins Wallet! + За да получиш наградата се нуждаеш от AppCoins Wallet. + + Плати като гост + Кредитна карта + PayPal + ПЛАТИ С PAYPAL + ПЛАТИ С КРЕДИТНА КАРТА + Номер на карта + ММ/ ГГ + CVV + CVC/CVV + СМЕНИ КАРТАТА + Плати с AppCoins Wallet + Вземи до %s%% бонус! + Ще получиш %s за тази покупка. + Ще получиш бонус за тази покупка. + НАЙ-ДОБРА СДЕЛКА + ГОТОВО! + Следващия път вземи до %s%% бонус с AppCoins Wallet! + Следващия път вземи бонус с AppCoins Wallet! + Можеше да получиш %s за тази покупка. + Покупката е завършена! + ДРУГИ ПЛАТЕЖНИ МЕТОДИ + Need help? + Contact Support. + + НАПРЕД + ОТКАЖИ + КУПИ + ИНСТАЛИРАЙ ПОРТФЕЙЛА + OK + Install + + Грешка + You already own this item! + Опа, нещо се обърка. + Възникна проблем с картата ти. Моля, опитай отново или се свържи с нас. + Транзакцията е отхвърлена от твоята банка. Моля, опитай с друга карта или се свържи с нас. + Изглежда нямаш достатъчно средства или пък имаш лимит на картата си. Моля, опитай с друга. + Изглежда, че картата ти е изтекла. Моля, опитай с друга. + Сигурен ли си, че номерът на картата ти е верен? Моля, опитай отново. + Типът на твоята карта все още не се поддържа. Опитай с друга. + Сигурен ли сте, че информацията за сигурност е вярна? Моля, опитай отново. + Твоят CVV/CVC код изглежда грешен. Моля, опитай отново. + Грешен CVV/CVC + Ако проблемът продължава, свържи се с нас. + diff --git a/app/src/main/res/external_strings/values-bg/perks.po b/app/src/main/res/external_strings/values-bg/perks.po new file mode 100644 index 00000000000..6ddfb1d38aa --- /dev/null +++ b/app/src/main/res/external_strings/values-bg/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: bg\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Bulgarian\n" +"Language: bg_BG\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Спечели AppCoins Credits!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-bn/external_strings.xml b/app/src/main/res/external_strings/values-bn/external_strings.xml new file mode 100644 index 00000000000..d43ec0d3e91 --- /dev/null +++ b/app/src/main/res/external_strings/values-bn/external_strings.xml @@ -0,0 +1,58 @@ + + + + To buy this item you first need to get the %s. + AppCoins Wallet + CLOSE + + You need the AppCoins Wallet to make this purchase. Download it from Aptoide or Play Store and come back to complete your purchase! + GOT IT! + + You need the AppCoins Wallet! + To get your reward you need the AppCoins Wallet. + + Pay as a guest + Credit Card + PayPal + PAY USING PAYPAL + PAY USING CREDIT CARD + Card number + MM/YY + সিভিভি + CVC/CVV + CHANGE CARD + Pay with AppCoins Wallet + Get up to %s%% Bonus! + You\'ll receive %s on this purchase. + You\'ll receive a Bonus on this purchase. + BEST DEAL + DONE! + Next time, get up to %s%% Bonus with the AppCoins Wallet! + Next time, get a Bonus with the AppCoins Wallet! + You could have received %s on this purchase. + Purchase completed! + MORE PAYMENT METHODS + Need help? + Contact Support. + + NEXT + বাতিল + BUY + INSTALL WALLET + ঠিক আছে + Install + + ত্রুটি + You already own this item! + Oops, something went wrong. + There was a problem with your card. Please try again or contact us. + The transaction has been rejected by your bank. Please try with a different card or contact us. + It seems you don\'t have enough funds or there\'s a limit on your card. Please try with a different one. + It seems your card has expired. Please try with a different one. + Are you sure your card number is correct? Please check and try again. + Your card type is not supported yet. Try with a different one. + Are you sure the security information is correct? Please try again. + Your CVV/CVC code seems to be wrong. Please try again. + Wrong CVV/CVC + If the problem persists please contact us. + diff --git a/app/src/main/res/external_strings/values-bn/perks.po b/app/src/main/res/external_strings/values-bn/perks.po new file mode 100644 index 00000000000..439f19390c5 --- /dev/null +++ b/app/src/main/res/external_strings/values-bn/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: bn\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Bengali\n" +"Language: bn_BD\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-de/external_strings.xml b/app/src/main/res/external_strings/values-de/external_strings.xml new file mode 100644 index 00000000000..f4fdbd7fae6 --- /dev/null +++ b/app/src/main/res/external_strings/values-de/external_strings.xml @@ -0,0 +1,58 @@ + + + + Um diesem Artikel kaufen zu können benötigen Sie das %s. + AppCoins Wallet + SCHLIESSEN + + Sie benötigen das AppCoins Wallet um diesen Einkauf tätigen zu können. Laden Sie es bei Aptoide oder Google Play Store herunter und kommen Sie zurück und schließen Sie den Einkauf ab! + GESCHAFFT! + + Sie benötigen das AppCoins Wallet! + Um diese Prämie erhalten zu können benötigen Sie das AppCoins Wallet. + + Bezahlung als Gast + Kreditkarte + PayPal + BEZAHLUNG MIT PAYPAL + BEZAHLUNG MIT KREDITKARTE + Kartennummer + MM/JJ + CVV + CVC/CVV + KARTE ÄNDERN + Mit AppCoins Wallet bezahlen + Erhalten Sie einen Prämie von bis zu %s%%! + Sie erhalten %s für diesen Einkauf. + Für diesen Einkauf erhalten Sie eine Prämie. + BESTER DEAL + ERLEDIGT! + Das nächste Mal erhalten Sie mit dem AppCoins Wallet einen Bonus von bis zu %s%%! + Das nächste Mal erhalten Sie mit dem AppCoins Wallet einen Bonus! + Sie hätten einen Bonus von %s auf diesen Einkauf erhalten können. + Einkauf abgeschlossen! + WEITERE BEZAHLMETHODEN + Sie benötigen Hilfe? + Support kontaktieren. + + WEITER + ABBRECHEN + KAUF + WALLET INSTALLIEREN + OK + Installieren + + Fehler + Sie haben diesen Artikel bereits gekauft! + Hoppla, etwas hat nicht funktioniert. + Es gab ein Problem mit ihrer Karte. Bitte nehmen Sie Kontakt zu uns auf. + Die Transaktion wurde von ihrer Bank zurückgewiesen. Bitte versuchen Sie es mit einer anderen Karte oder nehmen Sie Kontakt zu uns auf. + Scheinbar reicht ihr Guthaben nicht aus oder die Karte hat ein Limit. Bitte versuchen Sie es mit einer anderen. + Wie es scheint ist ihre Karte abgelaufen. Bitte versuchen Sie eine andere Karte. + Sind Sie sicher, dass ihre Kartennummer korrekt ist? Bitte überprüfen und erneut eingeben. + Ihre Karte wird noch nicht unterstützt. Bitte versuchen Sie es mit einer anderen Karte. + Sind Sie sicher, dass ihre Sicherheitsinformationen richtig sind? Bitte versuchen Sie es erneut. + Scheinbar ist ihr CVV/CVC-Code nicht korrekt. Bitte geben Sie ihn erneut ein. + Falscher CVV/CVC-Code + Nehmen Sie Kontakt zu uns auf, falls das Problem weiterbesteht. + diff --git a/app/src/main/res/external_strings/values-de/perks.po b/app/src/main/res/external_strings/values-de/perks.po new file mode 100644 index 00000000000..8fd221f390c --- /dev/null +++ b/app/src/main/res/external_strings/values-de/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: de\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: German\n" +"Language: de_DE\n" +"PO-Revision-Date: 2020-10-15 14:27\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "AppCoins Credits verdienen!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Kaufe bei {0} ein und erhalte {1} AppCoinsCredits." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Weil Sie in {0} etwas eingekauft haben." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Geben Sie {0} {1} in {2} Tagen und erhalten Sie {3} AppCoins Credits." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Da Sie in den letzten {2} Tagen mehr als {0} {1} ausgegeben haben." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Erreichen Sie den nächsten Level und erhalten Sie {0} AppCoins Credits!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Weil Sie den nächsten erreicht haben." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Erhalten einen Bonus von {0}% für alle deine Einkäufe, ausser AppCoins Credits bei {1}." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Erhalten einen Bonus von {0}% für deinen ersten Einkauf, ausser AppCoins Credits bei {1}" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "Wegen deines ersten Einkaufs bei {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Erhalten einen Bonus von {0}% für deinen ersten Einkauf des Tages, ausgenommen AppCoins Credits bei {1}" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "Wegen deines ersten Einkauf des Tages bei {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} AppCoins Credits Bonus!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Weil Du {0}{1} bei {2} ausgegeben hast." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-el/external_strings.xml b/app/src/main/res/external_strings/values-el/external_strings.xml new file mode 100644 index 00000000000..fbe99db8225 --- /dev/null +++ b/app/src/main/res/external_strings/values-el/external_strings.xml @@ -0,0 +1,58 @@ + + + + Για την πραγματοποίηση αυτής της αγοράς, θα πρέπει πρώτα να λάβετε το %s. + AppCoins Wallet + ΚΛΕΙΣΙΜΟ + + Για αυτήν την αγορά, χρειάζεστε το AppCoins Wallet. Κατεβάστε το μέσω Aptoide ή Play Store και επιστρέψτε για να την ολοκληρώσετε! + ΤΟ ΚΑΤΑΛΑΒΑ! + + Χρειάζεστε το AppCoins Wallet! + Για να λάβετε την αμοιβή σας χρειάζεστε το AppCoins Wallet. + + Πληρωμή ως επισκέπτης + Πιστωτική Κάρτα + PayPal + ΠΛΗΡΩΜΗ ΜΕΣΩ PAYPAL + ΠΛΗΡΩΜΗ ΜΕΣΩ ΠΙΣΤΩΤΙΚΗΣ ΚΑΡΤΑΣ + Αριθμός κάρτας + ΜΜ/ΕΕ + CVV + CVC/CVV + ΑΛΛΑΓΗ ΚΑΡΤΑΣ + Πληρωμή μέσω AppCoins Wallet + Λάβετε έως %s%% Bonus! + Θα λάβετε %s με αυτήν την αγορά. + Θα λάβετε ένα Bonus σε αυτήν την αγορά. + ΚΑΛΥΤΕΡΗ ΠΡΟΣΦΟΡΑ + ΤΕΛΟΣ! + Την επόμενη φορά, λάβετε έως %s%% Bonus με το AppCoins Wallet! + Την επόμενη φορά, λάβετε ένα Bonus με το AppCoins Wallet! + Θα μπορούσατε να έχετε λάβει %s με αυτήν την αγορά. + Η αγορά ολοκληρώθηκε! + ΠΕΡΙΣΣΟΤΕΡΕΣ ΜΕΘΟΔΟΙ ΠΛΗΡΩΜΗΣ + Χρειάζεστε βοήθεια; + Επικοινωνήστε με την Υποστήριξη. + + ΕΠΟΜΕΝΟ + ΑΚΥΡΟ + ΑΓΟΡΑ + ΕΓΚΑΤΑΣΤΑΣΗ WALLET + OK + Εγκατάσταση + + Σφάλμα + Είστε ήδη κάτοχος αυτού του item! + Ωχ, κάτι πήγε στραβά. + Υπήρξε ένα πρόβλημα με την κάρτα σας. Παρακαλούμε, προσπαθήστε ξανά ή επικοινωνήστε μαζί μας. + Η συναλλαγή απορρίφθηκε από την τράπεζά σας. Παρακαλούμε, δοκιμάστε μία διαφορετική κάρτα ή επικοινωνήστε μαζί μας. + Φαίνεται ότι δεν έχετε αρκετά χρήματα ή υπάρχει όριο στην κάρτα σας. Παρακαλούμε, δοκιμάστε με μία άλλη. + Φαίνεται ότι η κάρτα σας έχει λήξει. Παρακαλούμε, δοκιμάστε με μία άλλη. + Είστε βέβαιοι ότι ο αριθμός της κάρτας σας είναι σωστός; Παρακαλούμε, ελέγξτε και προσπαθήστε ξανά. + Ο τύπος της κάρτας σας δεν υποστηρίζεται ακόμη. Δοκιμάστε με μία άλλη. + Είστε βέβαιοι ότι οι πληροφορίες ασφαλείας είναι σωστές; Παρακαλούμε, δοκιμάστε ξανά. + Φαίνεται ότι ο κωδικός CVV/CVC είναι λανθασμένος. Παρακαλούμε, δοκιμάστε ξανά. + Εσφαλμένο CVV/CVC + Εάν το πρόβλημα επιμένει, παρακαλούμε επικοινωνήστε μαζί μας. + diff --git a/app/src/main/res/external_strings/values-el/perks.po b/app/src/main/res/external_strings/values-el/perks.po new file mode 100644 index 00000000000..925467ed386 --- /dev/null +++ b/app/src/main/res/external_strings/values-el/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: el\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Greek\n" +"Language: el_GR\n" +"PO-Revision-Date: 2020-10-15 14:27\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Κερδίστε AppCoins Credits!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Πραγματοποιήστε μία αγορά στο {0} και λάβετε {1} AppCoins Credits." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Επειδή πραγματοποιήσατε αγορά στο {0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Ξοδέψτε {0} {1} σε {2} ημέρες και λάβετε {3} AppCoins Credits." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Επειδή ξοδέψατε περισσότερα από {0} {1} τις τελευταίες {2} ημέρες." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Φτάστε στο επόμενο επίπεδο και λάβετε {0} AppCoins Credits!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Επειδή φτάσατε στο επόμενο επίπεδο." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Λάβετε επιπλέον {0}% Bonus σε όλες τις αγορές σας, εκτός αν γίνουν με AppCoins Credits, στο {1}." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Λάβετε επιπλέον {0}% Bonus στην πρώτη σας αγορά, εκτός αν γίνει με AppCoins Credits, στο {1}" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "Λόγω της πρώτης σας αγοράς στο {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Λάβετε επιπλέον {0}% Bonus στην πρώτη σας αγορά της ημέρας, εκτός αν γίνει με AppCoins Credits, στο {1}" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "Λόγω της πρώτης σας αγοράς της ημέρας στο {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} AppCoins Credits Bonus!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Επειδή ξοδέψατε {0} {1} στο {2}." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-es/external_strings.xml b/app/src/main/res/external_strings/values-es/external_strings.xml new file mode 100644 index 00000000000..1b612f548f2 --- /dev/null +++ b/app/src/main/res/external_strings/values-es/external_strings.xml @@ -0,0 +1,58 @@ + + + + Para hacer esta compra necesitas la %s. + AppCoins Wallet + CERRAR + + Necesitas la AppCoins Wallet para hacer esta compra. ¡Descárgala en Aptoide o Play Store y vuelve para completar la compra! + OK + + ¡Necesitas la AppCoins Wallet! + Necesitas la AppCoins Wallet para recibir tu premio. + + Pagar como visitante + Tarjeta bancaria + PayPal + USAR PAYPAL + USAR TARJETA + Número de la tarjeta + MM/AA + CVV + CVC/CVV + CAMBIAR TARJETA + Pagar con la AppCoins Wallet + ¡Recibe hasta un bonus de hasta el %s%%! + Recibirás %s por esta compra. + Recibirás un bonus por esta compra. + OFERTA + ¡COMPLETADO! + La próxima vez, recibe un bonus de hasta el %s%% con la AppCoins Wallet! + La próxima vez, recibe un bonus con la AppCoins Wallet! + Podrías haber recibido %s por esta compra. + ¡Compra completada! + MÁS MÉTODOS DE PAGO + ¿Necesitas ayuda? + Contacta con nosotros. + + SIGUIENTE + CANCELAR + COMPRAR + INSTALAR WALLET + OK + Instalar + + Error + ¡Ya tienes este objeto! + Ups, algo salió mal. + Hubo un problema con tu tarjeta. Por favor inténtalo de nuevo o ponte en contacto con nosotros. + La transacción ha sido rechazada por tu banco. Prueba con otra tarjeta o ponte en contacto con nosotros. + Parece que no tienes suficientes fondos o tu tarjeta tiene un límite. Prueba con otra tarjeta. + Tu tarjeta ha caducado. Prueba con otra. + ¿Estás seguro de que el número de la tarjeta es correcto? Compruébalo y prueba de nuevo. + Todavía no aceptamos este tipo de tarjeta. Prueba con otra. + ¿Estás seguro de que la información de seguridad es correcta? Prueba de nuevo. + El CVV/CVC no es correcto. Prueba de nuevo. + CVV/CVC no válido + Si el problema continúa, contáctanos: + diff --git a/app/src/main/res/external_strings/values-es/perks.po b/app/src/main/res/external_strings/values-es/perks.po new file mode 100644 index 00000000000..64e2e8bdda5 --- /dev/null +++ b/app/src/main/res/external_strings/values-es/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Spanish\n" +"Language: es_ES\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Gana AppCoins Credits!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Haz una compra en {0} y recibe {1} AppCoins Credits." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Porque hiciste una compra en {0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Gasta {0} {1} en {2} días y recibe {3} AppCoins Credits." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Porque gastaste más de {0} {1} en los últimos {2} días." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Alcanza el siguiente nivel y recibe {0} AppCoins Credits." + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Porque alcanzaste el siguiente nivel." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "¡Un bonus de {0} AppCoins Credits!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-fa/external_strings.xml b/app/src/main/res/external_strings/values-fa/external_strings.xml new file mode 100644 index 00000000000..f56de1fa3d5 --- /dev/null +++ b/app/src/main/res/external_strings/values-fa/external_strings.xml @@ -0,0 +1,58 @@ + + + + برای خرید این آیتم، ابتدا باید %s را بگیرید. + AppCoins Wallet + بستن + + برای این خرید نیاز به AppCoins Wallet دارید. آن را از Aptoide یا Play Store دانلود کنید و برای تکمیل خرید خود دوباره بازگردید! + متوجه شدم! + + نیاز به AppCoins Wallet دارید! + برای دریافت پاداش خود نیاز به AppCoins Wallet دارید. + + پرداخت به عنوان مهمان + کارت اعتباری + پی‌پال + پرداخت با پی‌پال + پرداخت با کارت اعتباری + شماره کارت + MM/YY + CVV + CVC/CVV + تغییر کارت + پرداخت با AppCoins Wallet + تا %s%% پاداش بگیرید! + در این خرید %s دریافت خواهید کرد. + با این خرید پاداشی دریافت خواهید کرد. + بهترین پیشنهاد + تمام! + سری بعد تا %s%% پاداش با AppCoins Wallet بگیرید! + سری بعد پاداشی با AppCoins Wallet بگیرید! + در این خرید می‌توانستید %s دریافت کنید. + خرید تکمیل شد! + سایر روش‌های پرداخت + نیاز به کمک دارید؟ + تماس با پشتیبانی. + + بعدی + لغو + خرید + نصب کیف پول + اوکی + نصب + + خطا + این آیتم را از قبل دارید! + مشکلی پیش آمده است. + مشکلی در ارتباط با کارت شما وجود دارد. لطفا دوباره امتحان کنید یا با ما تماس بگیرید. + تراکنش از طرف بانک شما رد شده است. لطفا با کارت دیگری دوباره امتحان کنید یا با ما تماس بگیرید. + به نظر می‌رسد موجودی کارت شما کافی نیست و یا محدودیتی در کارت شما وجود دارد. لطفا با کارت دیگری امتحان کنید. + به نظر می‌رسد کارت شما منقضی شده است. لطفا کارت دیگری را امتحان کنید. + آیا مطمئن هستید که اطلاعات کارت شما صحیح است؟ لطفا بررسی کرده و مجدد امتحان کنید. + نوع کارت شما پشتیبانی نمی‌شود. با کارت دیگری امتحان کنید. + آیا مطمئن هستید که اطلاعات امنیتی صحیح است؟ لطفا مجدد امتحان کنید. + کد CVV/CVC به نظر اشتباه است. لطفا دوباره امتحان کنید. + CVV/CVC اشتباه + اگر مشکل ادامه داشت لطفا با ما تماس بگیرید. + diff --git a/app/src/main/res/external_strings/values-fa/perks.po b/app/src/main/res/external_strings/values-fa/perks.po new file mode 100644 index 00000000000..325203271c8 --- /dev/null +++ b/app/src/main/res/external_strings/values-fa/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: fa\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Persian\n" +"Language: fa_IR\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "کسب اعتبار AppCoins!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "در {0} خریدی انجام دهید و {1} AppCoins Credits دریافت کنید." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "زیرا شما در {0} خریدی انجام داده اید." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "در{2} روز، {0} {1} خرج کنید و {3} AppCoins Credits بدست آورید." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "زیرا در{2} روز اخیر، بیش از {0} {1} خرج کرده‌اید." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "به مرحله بعدی برسید و {0} AppCoins Credits بدست آورید!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "زیرا به مرحله بعد رسیده‌اید." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} پاداش AppCoins Credits!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "زیرا شما {0} {1} را در {2} پرداخت کردید." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-fil/external_strings.xml b/app/src/main/res/external_strings/values-fil/external_strings.xml new file mode 100644 index 00000000000..c5772f28f7b --- /dev/null +++ b/app/src/main/res/external_strings/values-fil/external_strings.xml @@ -0,0 +1,58 @@ + + + + To buy this item you first need to get the %s. + AppCoins Wallet + CLOSE + + You need the AppCoins Wallet to make this purchase. Download it from Aptoide or Play Store and come back to complete your purchase! + GOT IT! + + You need the AppCoins Wallet! + To get your reward you need the AppCoins Wallet. + + Pay as a guest + Credit Card + PayPal + PAY USING PAYPAL + PAY USING CREDIT CARD + Card number + MM/YY + CVV + CVC/CVV + CHANGE CARD + Pay with AppCoins Wallet + Get up to %s%% Bonus! + You\'ll receive %s on this purchase. + You\'ll receive a Bonus on this purchase. + BEST DEAL + DONE! + Next time, get up to %s%% Bonus with the AppCoins Wallet! + Next time, get a Bonus with the AppCoins Wallet! + You could have received %s on this purchase. + Purchase completed! + MORE PAYMENT METHODS + Need help? + Contact Support. + + NEXT + CANCEL + BUY + INSTALL WALLET + OK + Install + + Error + You already own this item! + Oops, something went wrong. + There was a problem with your card. Please try again or contact us. + The transaction has been rejected by your bank. Please try with a different card or contact us. + It seems you don\'t have enough funds or there\'s a limit on your card. Please try with a different one. + It seems your card has expired. Please try with a different one. + Are you sure your card number is correct? Please check and try again. + Your card type is not supported yet. Try with a different one. + Are you sure the security information is correct? Please try again. + Your CVV/CVC code seems to be wrong. Please try again. + Wrong CVV/CVC + If the problem persists please contact us. + diff --git a/app/src/main/res/external_strings/values-fil/perks.po b/app/src/main/res/external_strings/values-fil/perks.po new file mode 100644 index 00000000000..404c45807fc --- /dev/null +++ b/app/src/main/res/external_strings/values-fil/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: fil\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Filipino\n" +"Language: fil_PH\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-fr/external_strings.xml b/app/src/main/res/external_strings/values-fr/external_strings.xml new file mode 100644 index 00000000000..1952f4caa2a --- /dev/null +++ b/app/src/main/res/external_strings/values-fr/external_strings.xml @@ -0,0 +1,58 @@ + + + + Pour acheter cet article, vous devez d\'abord obtenir les %s. + Portefeuille AppCoins + FERMER + + Vous avez besoin du Portefeuille AppCoins pour effectuer cet achat. Téléchargez-le sur Aptoide ou le Play Store et revenez pour finaliser votre achat ! + COMPRIS ! + + Vous avez besoin du Porte-monnaie AppCoins ! + Pour obtenir votre récompense, vous avez besoin du porte-monnaie AppCoins. + + Payez en tant qu\'invité + Carte de crédit + PayPal + PAYEZ EN UTILISANT PAYPAL + PAYEZ EN UTILISANT UNE CARTE DE CREDIT + Numéro de carte + MM/AA + CVV + CVC/CVV + CHANGER DE CARTE + Payez avec le Portefeuille Appcoins + Obtenez jusqu\'à %s%% de Bonus ! + Vous recevrez %s pour cet achat. + Vous recevrez un bonus pour cet achat. + MEILLEURE AFFAIRE + FAIT ! + La prochaine fois, obtenez jusqu\'à %s%% de Bonus avec le portefeuille AppCoins ! + La prochaine fois, obtenez un bonus avec le portefeuille AppCoins ! + Vous auriez reçu %s pour cet achat. + Achat terminé ! + PLUS DE METHODES DE PAIEMENT + Besoin d\'aide ? + Contact Support. + + SUIVANT + ANNULER + ACHETER + INSTALLER LE PORTEFEUILLE + OK + Installer + + Erreur + Vous possédez déjà cet article ! + Oups, quelque chose s\'est mal passé. + Il y a eu un problème avec votre carte. Veuillez réessayer ou nous contacter. + La transaction a été rejetée par votre banque. Veuillez essayer avec une autre carte ou contactez-nous. + Il semble que vous n\'ayez pas assez de fonds ou qu\'il y ait une limite à votre carte. Merci d\'essayer avec une autre carte. + Il semble que votre carte ait expiré. Veuillez essayer avec une autre carte. + Êtes-vous sûr que votre numéro de carte est correct ? Veuillez vérifier et réessayer. + Votre type de carte n\'est pas encore pris en charge. Essayez avec une autre carte. + Êtes-vous sûr que les informations de sécurité sont correctes ? Veuillez réessayer. + Votre code CVV/CVC semble être erroné. Veuillez réessayer. + Mauvais CVV/CVC + Si le problème persiste, veuillez nous contacter. + diff --git a/app/src/main/res/external_strings/values-fr/perks.po b/app/src/main/res/external_strings/values-fr/perks.po new file mode 100644 index 00000000000..e01548a5641 --- /dev/null +++ b/app/src/main/res/external_strings/values-fr/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: fr\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: French\n" +"Language: fr_FR\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Gagnez des Crédits AppCoins!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Faites un achat en {0} et recevez des AppCoins Credits en {1}." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Parce que vous avez fait un achat en {0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Dépensez {0} {1} en {2} jours et recevez {3} AppCoins Credits." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Parce que vous avez dépensé plus de {0} {1} dans les derniers {2} jours." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Atteignez le niveau suivant et recevez {0} Appcoins Credits !" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Parce que vous avez atteint le niveau suivant." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Recevez un bonus supplémentaire de {0}% sur tous vos achats dans {1}, à l'exception des AppCoins Credits." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Recevez un bonus supplémentaire de {0}% pour votre premier achat dans {1}, à l'exception des AppCoins Credits" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "Pour votre premier achat dans {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Recevez un bonus supplémentaire de {0}% lors de votre premier achat de la journée dans {1}, à l'exception des AppCoins Credits" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "Pour votre premier achat du jour dans {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} de Bonus en AppCoins Credits !" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Parce que vous avez dépensé {0} {1} dans {2}." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-hi/external_strings.xml b/app/src/main/res/external_strings/values-hi/external_strings.xml new file mode 100644 index 00000000000..feff4246ded --- /dev/null +++ b/app/src/main/res/external_strings/values-hi/external_strings.xml @@ -0,0 +1,58 @@ + + + + यह आइटम खरीदने के लिए आपको सबसे पहले %s प्राप्त करना होगा। + AppCoins Wallet + बंद करें + + इस खरीद के लिए आपको AppCoins Wallet की जरूरत है। इसे Aptoide या प्ले स्टोर से डाउनलोड करें और अपनी खरीद पूरी करने के लिए वापस आएँ! + समझ गए! + + आपको AppCoins Wallet की जरूरत है! + अपना इनाम पाने के लिए आपको AppCoins Wallet की जरूरत है। + + गेस्ट के रूप में भुगतान करें + क्रेडिट कार्ड + पेपैल + पेपैल से भुगतान करें + क्रेडिट कार्ड से भुगतान करें + कार्ड नंबर + MM/YY + सीवीवी + सीवीसी/सीवीवी + कार्ड बदलें + AppCoins Wallet से भुगतान करें + %s%% तक बोनस पाएँ! + आप इस खरीद पर %s प्राप्त करेंगे। + आप इस खरीद पर बोनस प्राप्त करेंगे। + बेस्ट डील + हो गया! + अगली बार, AppCoins Wallet के साथ %s%% तक बोनस प्राप्त करें! + अगली बार, AppCoins Wallet के साथ एक बोनस प्राप्त करें! + आप इस खरीद पर %s प्राप्त कर सकते थे। + खरीद पूरी हुई! + अधिक भुगतान विधि + मदद चाहिए? + सहायता टीम से संपर्क करें। + + आगे + रद्द करें + खरीदें + वॉलेट स्थापित करें + ठीक + स्थापित करें + + त्रुटि + यह वस्तु आपके पास पहले से है! + अरे, कुछ गलत हो गया है। + आपके कार्ड के साथ कोई समस्या थी। कृपया पुनः प्रयास करें या हमसे संपर्क करें। + बैंक द्वारा आपका लेनदेन अस्वीकार कर दिया गया है। कृपया एक अलग कार्ड से प्रयास करें या हमसे संपर्क करें। + शायद आपके पास पर्याप्त फंड नहीं है या आपके कार्ड पर कोई सीमा तय है। कृपया किसी दूसरे के साथ प्रयास करें। + शायद आपके कार्ड की अवधि समाप्त हो गई है। कृपया किसी दूसरे के साथ प्रयास करें। + क्या आपका कार्ड नंबर बिलकुल सही है? कृपया जाँच करें और पुनः प्रयास करें। + आपका कार्ड अभी समर्थित नहीं है। कृपया किसी दूसरे के साथ प्रयास करें। + क्या आपकी सुरक्षा जानकारी बिलकुल सही है? कृपया पुनः प्रयास करें। + आपका सीवीवी/सीवीसी कोड गलत लग रहा है। कृपया पुनः प्रयास करें। + गलत सीवीवी/सीवीसी + अगर समस्या रहती है तो कृपया हमसे संपर्क करें। + diff --git a/app/src/main/res/external_strings/values-hi/perks.po b/app/src/main/res/external_strings/values-hi/perks.po new file mode 100644 index 00000000000..8ea6810d492 --- /dev/null +++ b/app/src/main/res/external_strings/values-hi/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: hi\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Hindi\n" +"Language: hi_IN\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-hr-rHR/external_strings.xml b/app/src/main/res/external_strings/values-hr-rHR/external_strings.xml new file mode 100644 index 00000000000..c5772f28f7b --- /dev/null +++ b/app/src/main/res/external_strings/values-hr-rHR/external_strings.xml @@ -0,0 +1,58 @@ + + + + To buy this item you first need to get the %s. + AppCoins Wallet + CLOSE + + You need the AppCoins Wallet to make this purchase. Download it from Aptoide or Play Store and come back to complete your purchase! + GOT IT! + + You need the AppCoins Wallet! + To get your reward you need the AppCoins Wallet. + + Pay as a guest + Credit Card + PayPal + PAY USING PAYPAL + PAY USING CREDIT CARD + Card number + MM/YY + CVV + CVC/CVV + CHANGE CARD + Pay with AppCoins Wallet + Get up to %s%% Bonus! + You\'ll receive %s on this purchase. + You\'ll receive a Bonus on this purchase. + BEST DEAL + DONE! + Next time, get up to %s%% Bonus with the AppCoins Wallet! + Next time, get a Bonus with the AppCoins Wallet! + You could have received %s on this purchase. + Purchase completed! + MORE PAYMENT METHODS + Need help? + Contact Support. + + NEXT + CANCEL + BUY + INSTALL WALLET + OK + Install + + Error + You already own this item! + Oops, something went wrong. + There was a problem with your card. Please try again or contact us. + The transaction has been rejected by your bank. Please try with a different card or contact us. + It seems you don\'t have enough funds or there\'s a limit on your card. Please try with a different one. + It seems your card has expired. Please try with a different one. + Are you sure your card number is correct? Please check and try again. + Your card type is not supported yet. Try with a different one. + Are you sure the security information is correct? Please try again. + Your CVV/CVC code seems to be wrong. Please try again. + Wrong CVV/CVC + If the problem persists please contact us. + diff --git a/app/src/main/res/external_strings/values-hr-rHR/perks.po b/app/src/main/res/external_strings/values-hr-rHR/perks.po new file mode 100644 index 00000000000..cb3fb9458e3 --- /dev/null +++ b/app/src/main/res/external_strings/values-hr-rHR/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: hr\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Croatian\n" +"Language: hr_HR\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-hu/external_strings.xml b/app/src/main/res/external_strings/values-hu/external_strings.xml new file mode 100644 index 00000000000..933c60ee76f --- /dev/null +++ b/app/src/main/res/external_strings/values-hu/external_strings.xml @@ -0,0 +1,58 @@ + + + + To buy this item you first need to get the %s. + AppCoins Wallet + CLOSE + + You need the AppCoins Wallet to make this purchase. Download it from Aptoide or Play Store and come back to complete your purchase! + GOT IT! + + You need the AppCoins Wallet! + To get your reward you need the AppCoins Wallet. + + Pay as a guest + Credit Card + PayPal + PAY USING PAYPAL + PAY USING CREDIT CARD + Card number + MM/YY + CVV + CVC/CVV + KÁRTYA MÓDOSÍTÁSA + Pay with AppCoins Wallet + Get up to %s%% Bonus! + You\'ll receive %s on this purchase. + You\'ll receive a Bonus on this purchase. + BEST DEAL + DONE! + Next time, get up to %s%% Bonus with the AppCoins Wallet! + Next time, get a Bonus with the AppCoins Wallet! + You could have received %s on this purchase. + Purchase completed! + MORE PAYMENT METHODS + Need help? + Contact Support. + + NEXT + MÉGSE + BUY + INSTALL WALLET + OK + Install + + Hiba + You already own this item! + Oops, something went wrong. + There was a problem with your card. Please try again or contact us. + The transaction has been rejected by your bank. Please try with a different card or contact us. + It seems you don\'t have enough funds or there\'s a limit on your card. Please try with a different one. + It seems your card has expired. Please try with a different one. + Are you sure your card number is correct? Please check and try again. + Your card type is not supported yet. Try with a different one. + Are you sure the security information is correct? Please try again. + Your CVV/CVC code seems to be wrong. Please try again. + Wrong CVV/CVC + If the problem persists please contact us. + diff --git a/app/src/main/res/external_strings/values-hu/perks.po b/app/src/main/res/external_strings/values-hu/perks.po new file mode 100644 index 00000000000..772d9932a7c --- /dev/null +++ b/app/src/main/res/external_strings/values-hu/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: hu\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Hungarian\n" +"Language: hu_HU\n" +"PO-Revision-Date: 2020-10-15 14:27\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-in/external_strings.xml b/app/src/main/res/external_strings/values-in/external_strings.xml new file mode 100644 index 00000000000..6f73ea866a3 --- /dev/null +++ b/app/src/main/res/external_strings/values-in/external_strings.xml @@ -0,0 +1,58 @@ + + + + Untuk membeli item ini, terlebih dahulu Anda harus mendapatkan %s. + AppCoins Wallet + TUTUP + + Anda membutuhkan Dompet AppCoins untuk melakukan pembelian ini. Unduh dompet itu dari Aptoide atau Play Store, lalu kembali untuk menyelesaikan pembelian Anda! + MENGERTI! + + Anda membutuhkan Dompet AppCoin! + Untuk mendapatkan hadiah, Anda membutuhkan Dompet AppCoin. + + Bayar sebagai tamu + Kartu Kredit + PayPal + BAYAR DENGAN PAYPAL + BAYAR DENGAN KARTU KREDIT + Nomor kartu + MM/YY + CVV + CVC/CVV + GANTI KARTU + Bayar dengan Dompet AppCoin + Dapatkan Bonus hingga %s%%! + Anda akan menerima %s pada pembelian ini. + Anda akan menerima Bonus pada pembelian ini. + PENAWARAN TERBAIK + SELESAI! + Lain kali, dapatkan Bonus hingga %s%% dengan Dompet AppCoin! + Lain kali, dapatkan Bonus Dompet AppCoin! + Anda sebenarnya bisa menerima %s pada pembelian ini. + Pembelian selesai! + METODE PEMBAYARAN LAINNYA + Perlu bantuan? + Hubungi Dukungan. + + SELANJUTNYA + BATAL + BELI + INSTAL DOMPET + Oke + Install + + Kesalahan + Anda sudah punya item ini! + Ups, ada kesalahan. + Ada masalah dengan kartu Anda. Silakan coba lagi atau hubungi kami. + Transaksi ditolak oleh bank Anda. Silakan coba dengan kartu lain atau hubungi kami. + Sepertinya dana yang Anda miliki tidak cukup atau kartu Anda dibatasi. Silakan coba kartu lain. + Sepertinya kartu Anda sudah kedaluwarsa. Silakan coba kartu lain. + Apakah Anda yakin nomor kartu Anda benar? Silakan periksa dan coba lagi. + Jenis kartu Anda belum didukung. Silakan coba yang lainnya. + Apakah Anda yakin informasi keamanannya benar? Silakan coba lagi. + Kode CVV/CVC Anda sepertinya salah. Silakan coba lagi. + CVV/CVC salah + Kalau masalah tetap ada, silakan hubungi kami. + diff --git a/app/src/main/res/external_strings/values-in/perks.po b/app/src/main/res/external_strings/values-in/perks.po new file mode 100644 index 00000000000..349bb8f5dc3 --- /dev/null +++ b/app/src/main/res/external_strings/values-in/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: id\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Indonesian\n" +"Language: id_ID\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Dapatkan Kredit AppCoins!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Lakukan pembelian di {0} dan dapatkan {1} Kredit AppCoins." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Karena Anda melakukan pembelian di {0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Belanjakan {0} {1} dalam {2} hari dan dapatkan {3} Kredit AppCoins." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Karena Anda membelanjakan lebih dari {0} {1} dalam {2} hari terakhir." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Raih level berikutnya dan dapatkan {0} Kredit AppCoins!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Karena Anda sudah meraih level berikutnya." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Dapatkan Bonus tambahan sebesar {0}% pada semua pembelian Anda, kecuali Kredit AppCoins Credits, di {1}." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Dapatkan Bonus tambahan sebesar {0}% pada pembelian pertama Anda, kecuali Kredit AppCoins Credits, di {1}" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "Karena pembelian pertama Anda di {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Dapatkan Bonus tambahan sebesar {0}% pada pembelian pertama Anda hari ini, kecuali Kredit AppCoins Credits, di {1}" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "Karena pembelian pertama Anda hari ini di {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} Bonus Kredit AppCoins!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Karena Anda menghabiskan {0} {1} di {2}." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-it/external_strings.xml b/app/src/main/res/external_strings/values-it/external_strings.xml new file mode 100644 index 00000000000..7fd18347a61 --- /dev/null +++ b/app/src/main/res/external_strings/values-it/external_strings.xml @@ -0,0 +1,58 @@ + + + + Per acquistare questo articolo ti serve l\'%s. + AppCoins Wallet + CHIUDI + + Ti serve l\'AppCoins Wallet per effettuare questo acquisto. Scaricalo da Aptoide o Play Store e torna a trovarci per completare l\'acquisto! + HO CAPITO! + + Ti serve un AppCoins Wallet! + Per ricevere il tuo premio ti serve l\'AppCoins Wallet. + + Paga da ospite + Carta di credito + PayPal + PAGA CON PAYPAL + PAGA CON CARTA DI CREDITO + Numero di carta + MM/AA + CVV + CVC/CVV + MODIFICA CARTA + Paga con l\'AppCoins Wallet + Ricevi fino al %s%% di bonus! + Riceverai %s su questo acquisto. + Riceverai un bonus su questo acquisto. + OFFERTA MIGLIORE + FATTO! + La prossima volta ricevi fino al %s%% di bonus con l\'AppCoins Wallet! + La prossima volta ricevi un bonus con l\'AppCoins Wallet! + Avresti potuto ricevere %s su questo acquisto. + Acquisto completato! + ALTRI METODI DI PAGAMENTO + Serve aiuto? + Contatta l\'assistenza clienti. + + AVANTI + ANNULLA + ACQUISTA + INSTALLA IL WALLET + OK + Installa + + Errore + Hai già questo articolo! + Si è verificato un errore. + Sì è verificato un problema con la tua carta. Riprova o contattaci. + La transazione è stata rifiutata dalla tua banca. Prova con un\'altra carta o contattaci. + Sembra che tu non abbia abbastanza fondi o che ci sia un limite alla tua carta. Provane un\'altra. + Sembra che la tua carta sia scaduta. Provane un\'altra. + Hai controllato che i dati della tua carta siano corretti? Controlla e ritenta. + Questo tipo di carta non è supportato. Provane un\'altra. + Hai controllato che le informazioni di sicurezza siano corrette? Ritenta. + Il tuo codice CVV/CVC sembra errato. Ritenta. + CVV/CVC errato + Se il problema persiste, contattaci. + diff --git a/app/src/main/res/external_strings/values-it/perks.po b/app/src/main/res/external_strings/values-it/perks.po new file mode 100644 index 00000000000..e415e7d6a29 --- /dev/null +++ b/app/src/main/res/external_strings/values-it/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: it\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Italian\n" +"Language: it_IT\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Guadagna AppCoins Credits!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Effettua un acquisto in {0} e ricevi {1} AppCoins Credits." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Grazie al tuo acquisto in {0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Spendi {0} {1} in {2} giorni e ricevi {3} AppCoins Credits." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Grazie al tuo acquisto di più di {0} {1} negli ultimi {2} giorni." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Raggiungi il livello successivo e ricevi {0} AppCoins Credits!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Grazie al raggiungimento del livello successivo." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Ricevi un bonus extra del {0}% su tutti i tuoi acquisti, tranne quelli in AppCoins Credits, in {1}." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Ricevi un bonus extra del {0}% sul tuo primo acquisto, tranne quelli in AppCoins Credits, in {1}" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "Grazie al tuo primo acquisto in {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Ricevi un bonus extra del {0}% sul tuo primo acquisto della giornata, tranne quelli in AppCoins Credits, in {1}" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "Grazie al tuo primo acquisto della giornata in {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} AppCoins Credits di bonus!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Grazie alla tua spesa di {0} {1} in {2}." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-ja/external_strings.xml b/app/src/main/res/external_strings/values-ja/external_strings.xml new file mode 100644 index 00000000000..6736164bb61 --- /dev/null +++ b/app/src/main/res/external_strings/values-ja/external_strings.xml @@ -0,0 +1,58 @@ + + + + To buy this item you first need to get the %s. + AppCoins Wallet + CLOSE + + You need the AppCoins Wallet to make this purchase. Download it from Aptoide or Play Store and come back to complete your purchase! + GOT IT! + + You need the AppCoins Wallet! + To get your reward you need the AppCoins Wallet. + + Pay as a guest + Credit Card + PayPal + PAY USING PAYPAL + PAY USING CREDIT CARD + Card number + MM/YY + CVV + CVC/CVV + カードの変更 + Pay with AppCoins Wallet + Get up to %s%% Bonus! + You\'ll receive %s on this purchase. + You\'ll receive a Bonus on this purchase. + BEST DEAL + DONE! + Next time, get up to %s%% Bonus with the AppCoins Wallet! + Next time, get a Bonus with the AppCoins Wallet! + You could have received %s on this purchase. + Purchase completed! + MORE PAYMENT METHODS + Need help? + Contact Support. + + NEXT + キャンセルする + BUY + INSTALL WALLET + わかりました + Install + + エラー + You already own this item! + Oops, something went wrong. + There was a problem with your card. Please try again or contact us. + The transaction has been rejected by your bank. Please try with a different card or contact us. + It seems you don\'t have enough funds or there\'s a limit on your card. Please try with a different one. + It seems your card has expired. Please try with a different one. + Are you sure your card number is correct? Please check and try again. + Your card type is not supported yet. Try with a different one. + Are you sure the security information is correct? Please try again. + Your CVV/CVC code seems to be wrong. Please try again. + Wrong CVV/CVC + If the problem persists please contact us. + diff --git a/app/src/main/res/external_strings/values-ja/perks.po b/app/src/main/res/external_strings/values-ja/perks.po new file mode 100644 index 00000000000..78a3de6e3ff --- /dev/null +++ b/app/src/main/res/external_strings/values-ja/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: ja\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Japanese\n" +"Language: ja_JP\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-ko/external_strings.xml b/app/src/main/res/external_strings/values-ko/external_strings.xml new file mode 100644 index 00000000000..d7d592d7a20 --- /dev/null +++ b/app/src/main/res/external_strings/values-ko/external_strings.xml @@ -0,0 +1,58 @@ + + + + To buy this item you first need to get the %s. + AppCoins Wallet + CLOSE + + You need the AppCoins Wallet to make this purchase. Download it from Aptoide or Play Store and come back to complete your purchase! + GOT IT! + + You need the AppCoins Wallet! + To get your reward you need the AppCoins Wallet. + + Pay as a guest + Credit Card + PayPal + PAY USING PAYPAL + PAY USING CREDIT CARD + Card number + MM/YY + CVV + CVC/CVV + 카드 변경 + Pay with AppCoins Wallet + Get up to %s%% Bonus! + You\'ll receive %s on this purchase. + You\'ll receive a Bonus on this purchase. + BEST DEAL + DONE! + Next time, get up to %s%% Bonus with the AppCoins Wallet! + Next time, get a Bonus with the AppCoins Wallet! + You could have received %s on this purchase. + Purchase completed! + MORE PAYMENT METHODS + Need help? + Contact Support. + + NEXT + 취소 + BUY + INSTALL WALLET + OK + Install + + 오류 + You already own this item! + Oops, something went wrong. + There was a problem with your card. Please try again or contact us. + The transaction has been rejected by your bank. Please try with a different card or contact us. + It seems you don\'t have enough funds or there\'s a limit on your card. Please try with a different one. + It seems your card has expired. Please try with a different one. + Are you sure your card number is correct? Please check and try again. + Your card type is not supported yet. Try with a different one. + Are you sure the security information is correct? Please try again. + Your CVV/CVC code seems to be wrong. Please try again. + Wrong CVV/CVC + If the problem persists please contact us. + diff --git a/app/src/main/res/external_strings/values-ko/perks.po b/app/src/main/res/external_strings/values-ko/perks.po new file mode 100644 index 00000000000..5538f5491ae --- /dev/null +++ b/app/src/main/res/external_strings/values-ko/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: ko\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Korean\n" +"Language: ko_KR\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-mr/external_strings.xml b/app/src/main/res/external_strings/values-mr/external_strings.xml new file mode 100644 index 00000000000..5b2480d2273 --- /dev/null +++ b/app/src/main/res/external_strings/values-mr/external_strings.xml @@ -0,0 +1,58 @@ + + + + ही वस्तु खरेदी करण्यासाठी आपणांस प्रथम %s मिळवावे लागेल. + अॅपकॉइन्स वॉलेट + बंद करा + + ही खरेदी करण्यासाठी आपल्याला अॅपकॉइन्स वॉलेटची गरज आहे. ह्याला Aptoide किंवा प्लेस्टोरमधुन डाऊनलोड करुन मग खरेदी पूर्ण करण्यासाठी परत या! + समजल! + + आपल्याला AppCoins Walletची गरज आहे! + बक्षिस मिळविण्यासाठी आपल्याला AppCoins Walletची गरज आहे. + + पाहुणे म्हणुन पैसे भरा + क्रेडीट कार्ड + पेपाल + पेपालद्वारे पैसे भरा + क्रेडीट कार्डाद्वारे पैसे भरा + कार्ड क्रमांक + महिना/वर्ष + सीव्हीव्ही + सीव्हीव्ही/सीव्हीव्ही + कार्ड बदला + अॅपकॉइन्स वॉलेटमधुन पैसे भरा + %s%% पर्यंत बोनस मिळवा! + ह्या खरेदीवर आपणांस %s मिळतील. + ह्या खरेदीवर आपणांस बोनस मिळेल. + सर्वोत्तम डील + झाले! + पुढच्या वेळी, अॅपकॉइन्स वॉलेटसह %s%% पर्यंत बोनस मिळवा! + पुढच्या वेळी, अॅपकॉइन्स वॉलेटसह बोनस मिळवा! + ह्या खरेदीवर आपणांस %s मिळु शकले असते. + खरेदी पूर्ण झाली! + अन्य पेमेंट पद्धती + मदत हवीय? + समर्थनाशी संपर्क साधा. + + पुढे + रद्द करा + खरेदी करा + वॉलेट इन्स्टॉल करा + ठीक आहे + इंस्टॉल करा + + त्रुटी + ही वस्तु आधीच आपल्या मालकीची आहे! + अरेरे, काहीतरी त्रुटी आली. + आपल्या कार्डात समस्या आली. कृपया पुन्हा प्रयत्न करा किंवा आमच्याशी संपर्क साधा. + आपल्या बँकेने व्यवहार नाकारला आहे. कृपया वेगळ्या कार्डासह प्रयत्न करा किंवा आमच्याशी संपर्क साधा. + आपल्याकडे पुरेसे पैसे नाहीत किंवा आपल्या कार्डावर सीमा आहे असे दिसतेय. कृपया वेगळ्या कार्डानिशी प्रयत्न करा. + आपले कार्ड कालबाह्य झाले आहे असे दिसतेय. कृपया वेगळे वापरुन प्रयत्न करा. + आपला कार्ड नंबर बरोबर आहे ह्याची खात्री आहे? कृपया तपासुन पुन्हा प्रयत्न करा. + आपल्या कार्डाचा प्रकार अजुनतरी समर्थित नाही. वेगळे वापरुन प्रयत्न करा. + सुरक्षा माहिती बरोबर आहे ह्याची खात्री आहे? कृपया पुन्हा प्रयत्न करा. + आपला सीव्हीव्ही/सीव्हीसी कोड चुकीचा वाटतोय. कृपया पुन्हा प्रयत्न करा. + चुकीचा सीव्हीव्ही/सीव्हीसी + जर समस्या तशीच असेल तर कृपया आमच्याशी संपर्क साधा. + diff --git a/app/src/main/res/external_strings/values-mr/perks.po b/app/src/main/res/external_strings/values-mr/perks.po new file mode 100644 index 00000000000..5a53e0df794 --- /dev/null +++ b/app/src/main/res/external_strings/values-mr/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: mr\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Marathi\n" +"Language: mr_IN\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "अॅपकॉइन्स क्रेडीटस मिळवा!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "{0} मध्ये खरेदी करा आणि {1} अॅपकॉइन्स क्रेडीटस मिळवा." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "कारण आपण {0} मध्ये खरेदी केलीत." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "{2} दिवसात {0} {1} खर्च करा आणि {3} अॅपकॉइन्स क्रेडीटस मिळवा." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "कारण आपण गेल्या {2} दिवसात {0} {1} पेक्षा जास्त खर्च केलात." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "पुढच्या पातळीवर पोचा आणि {0} अॅपकॉइन्स क्रेडीटस मिळवा!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "कारण आपण पुढच्या पातळीवर पोचलात." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "{1} मधीलआपल्या सर्व खरेदीवर {0}% अतिरिक्त बोनस मिळवा अपवाद फक्त अॅपकॉइन्स क्रेडीटसचा,." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "{1} मधील खरेदीत, {0}% अतिरिक्त बोनस मिळवा, अपवाद फक्त अॅपकॉइन्स क्रेडीटसचा" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "{0} मधील आपल्या पहल्या खरेदीसाठी." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "{1} मधील दिवसाच्या आपल्या पहिल्या खरेदीवर {0}% अतिरिक्त बोनस मिळवा, अपवाद फक्त अॅपकॉइन्स क्रेडीटसचा" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "{0} मधील आपल्या पहिल्या खरेदीसाठी." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} अॅपकॉइन्स क्रेडीट बोनस!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "{2} मध्ये आपण {0} {1} खर्च केलेत म्हणुन." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-ms/external_strings.xml b/app/src/main/res/external_strings/values-ms/external_strings.xml new file mode 100644 index 00000000000..a5fb103b275 --- /dev/null +++ b/app/src/main/res/external_strings/values-ms/external_strings.xml @@ -0,0 +1,58 @@ + + + + Untuk beli item ini, anda perlu dapatkan %s terlebih dahulu. + Dompet AppCoins + TUTUP + + Anda perlu Dompet AppCoins untuk membuat pembelian ini. Muat turun daripada Aptoide atau Play Store dan kembali semula untuk lengkapkan pembelian anda! + OKEY! + + Anda perlukan Dompet AppCoins! + Untuk dapatkan ganjaran anda, anda perlu Dompet AppCoins. + + Bayar sebagai tetamu + Kad Kredit + PayPal + BAYAR DENGAN PAYPAL + BAYAR DENGAN KAD KREDIT + Nombor kad + BB/TT + CVV + CVC/CVV + TUKAR KAD + Bayar dengan Dompet AppCoins + Dapatkan sehingga %s%% Bonus! + Anda akan terima %s bagi pembelian ini. + Anda akan dapat Bonus atas pembelian ini. + TAWARAN TERBAIK + SELESAI! + Lain kali, dapatkan sehingga %s%% Bonus dengan Dompet AppCoins! + Lain kali, dapatkan Bonus dengan Dompet AppCoins! + Anda berpeluang untuk terima %s bagi pembelian ini. + Pembelian selesai! + KAEDAH BAYARAN LAIN + Perlu bantuan? + Hubungi Sokongan. + + SETERUSNYA + BATAL + BELI + PASANG DOMPET + OK + Pasang + + Ralat + Anda sudah ada item ini! + Oops, ada sesuatu yang tidak kena. + Ada masalah dengan kad anda. Sila cuba lagi atau hubungi kami. + Transaksi telah ditolak oleh bank anda. Sila cuba kad yang lain atau hubungi kami. + Nampaknya anda tidak mempunyai dana yang cukup atau ada had pada kad anda. Sila cuba kad yang lain. + Nampaknya kad anda telah luput. Sila cuba kad yang lain. + Anda pasti nombor kad anda betul? Sila semak dan cuba lagi. + Jenis kad anda masih belum disokong. Cuba dengan kad lain. + Anda pasti butiran keselamatan ini betul? Sila cuba lagi. + Kod CVV/CVC anda mungkin tidak betul. Sila cuba lagi. + Salah CVV/CVC + Jika masalah berlanjutan, sila hubungi kami. + diff --git a/app/src/main/res/external_strings/values-ms/perks.po b/app/src/main/res/external_strings/values-ms/perks.po new file mode 100644 index 00000000000..5736f2d5ad7 --- /dev/null +++ b/app/src/main/res/external_strings/values-ms/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: ms\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Malay\n" +"Language: ms_MY\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Peroleh Kredit AppCoins!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Buat pembelian dalam {0} dan terima {1} Kredit AppCoins." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Kerana anda telah buat pembelian dalam {0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Belanja {0} {1} dalam tempoh {2} hari dan terima {3} Kredit AppCoins." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Kerana anda telah belanja lebih daripada {0}{1} dalam tempoh {2} yang lepas." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Capai tahap seterusnya dan terima {0} Kredit AppCoins!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Kerana anda telah capai tahap seterusnya." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Terima bonus ekstra sebanyak {0}% bagi semua pembelian anda, kecuali Kredit AppCoins, dalam {1}." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Terima bonus ekstra sebanyak {0}% untuk pembelian pertama anda, kecuali Kredit AppCoins dalam {1}" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "Atas pembelian pertama anda dalam {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Terima bonus ekstra sebanyak {0}% untuk pembelian pertama anda untuk hari ini, kecuali Kredit AppCoins dalam {1}" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "Atas pembelian pertama anda untuk hari ini dalam {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} Bonus Kredit AppCoins!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Kerana anda belanja {0} {1} dalam {2}." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-my/external_strings.xml b/app/src/main/res/external_strings/values-my/external_strings.xml new file mode 100644 index 00000000000..e350f5abca9 --- /dev/null +++ b/app/src/main/res/external_strings/values-my/external_strings.xml @@ -0,0 +1,58 @@ + + + + ဤပစၥည္းကို ၀ယ္ရန္အတြက္ %s ကို ဦးစြာရယူရန္ လိုအပ္ပါသည္။ + AppCoins Wallet + ပိတ္မည္ + + ဤ၀ယ္ယူမႈကို ျပဳလုပ္ရန္အတြက္ AppCoins Wallet ကို လိုအပ္ပါသည္။ သင့္၀ယ္ယူမႈ ၿပီးစီးေစရန္အတြက္ Aptoide သို႔မဟုတ္ Play Store မွ ေဒါင္းလုဒ္ဆြဲကာ ျပန္လာခဲ့ပါ။ + ရပါၿပီ။ + + AppCoins Wallet ကို သင္လိုအပ္ပါသည္။ + သင့္ဆုကိုရယူရန္ AppCoins Wallet ကို သင္လိုအပ္ပါသည္။ + + ဧည့္သည္အေနျဖင့္ ေပးေခ်မည္ + ခရက္ဒစ္ကဒ္ + PayPal + PAYPAL ျဖင့္ ေပးေခ်မည္ + ခရက္ဒစ္ကဒ္ျဖင့္ ေပးေခ်မည္ + ကဒ္နံပါတ္ + လ/ႏွစ္ + CVV + CVC/CVV + ကဒ္ကိုေျပာင္းမည္ + AppCoins Wallet ျဖင့္ ေပးေခ်မည္ + %s%% အထိ ေဘာနပ္စ္ရယူလိုက္ပါ။ + ဤ၀ယ္ယူမႈအေပၚ %s သင္ရရွိပါမည္။ + ဤ၀ယ္ယူမႈအေပၚ ေဘာနပ္စ္တစ္ခု သင္ရရွိပါမည္။ + အေကာင္းဆံုးေစ်းႏႈန္း + ၿပီးပါၿပီ။ + ေနာက္တစ္ႀကိမ္တြင္ AppCoins Wallet ျဖင့္ %s%% အထိ ေဘာနပ္စ္ရယူလိုက္ပါ။ + ေနာက္တစ္ႀကိမ္တြင္ AppCoins Wallet ျဖင့္ ေဘာနပ္စ္တစ္ခု ရယူလိုက္ပါ။ + ဤ၀ယ္ယူမႈအေပၚ %s သင္ရရွိနိုင္ပါသည္။ + ၀ယ္ယူမႈ ၿပီးပါၿပီ။ + ေနာက္ထပ္ ေငြေပးေခ်ရန္ နည္းလမ္းမ်ား + အကူအညီ လိုအပ္ပါသလား။ + ပံ့ပိုးမႈအား ဆက္သြယ္ပါ။ + + ေနာက္ထပ္ + ပယ္ဖ်က္မည္ + ၀ယ္မည္ + WALLET ကို ထည့္သြင္းမည္ + အိုေက + ထည့္သြင္းပါ + + ခ်ိဳ႕ယြင္းခ်က္ + ဤပစၥည္းကို သင္ပိုင္ဆိုင္ထားၿပီး ျဖစ္ပါသည္။ + အိုး တခုခုမွားသြားပါသည္။ + သင့္ကဒ္တြင္ ျပႆနာတစ္ခု ရွိခဲ့ပါသည္။ ျပန္ႀကိဳးစားေပးပါ သို့မဟုတ္ ကၽြႏ္ုပ္တို့ကို ဆက္သြယ္ေပးပါ။ + ေငြလႊဲေျပာင္းမႈကို သင့္ဘဏ္က ျငင္းဆန္လိုက္ပါသည္။ အျခားကဒ္တစ္ကဒ္ျဖင့္ ႀကိဳးစားေပးပါ သို့မဟုတ္ ကၽြႏ္ုပ္တို့ကို ဆက္သြယ္ေပးပါ။ + သင့္ကဒ္တြင္ ေငြအလံုအေလာက္မရွိျခင္း သို့မဟုတ္ ကန့္သတ္ခ်က္တစ္ခု ရွိေနျခင္း ျဖစ္ပံုရပါသည္။ အျခားကဒ္တစ္ကဒ္ျဖင့္ ႀကိဳးစားေပးပါ။ + သင့္ကဒ္သည္ သက္တမ္းကုန္ေနပံု ရပါသည္။ အျခားကဒ္တစ္ကဒ္ျဖင့္ ႀကိဳးစားေပးပါ။ + သင့္ကဒ္နံပါတ္ မွန္ကန္ေၾကာင္း ေသခ်ာပါသလား။ ျပန္လည္စစ္ေဆးကာ ျပန္ႀကိဳးစားေပးပါ။ + သင့္ကဒ္အမ်ိဳးအစားကို မပံ့ပိုးေပးထားေသးပါ။ အျခားကဒ္တစ္ကဒ္ျဖင့္ ႀကိဳးစားပါ။ + လံုၿခံဳေရး သတင္းအခ်က္အလက္ မွန္ကန္ေၾကာင္း ေသခ်ာပါသလား။ ျပန္ႀကိဳးစားေပးပါ။ + သင့္ CVV/CVC သေကၤတ မွားယြင္းေနပံု ရပါသည္။ ျပန္ႀကိဳးစားေပးပါ။ + CVV/CVC မွားေနပါသည္ + ျပႆနာဆက္ရွိေနပါက ကၽြႏ္ုပ္တို႔ကို ဆက္သြယ္ေပးပါ။ + diff --git a/app/src/main/res/external_strings/values-my/perks.po b/app/src/main/res/external_strings/values-my/perks.po new file mode 100644 index 00000000000..272380c8683 --- /dev/null +++ b/app/src/main/res/external_strings/values-my/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: my\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Burmese\n" +"Language: my_MM\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "AppCoins Credits ကို ရယူလိုက္ပါ။" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "{0} တြင္ ဝယ္ယူမႈတစ္ခု ျပဳလုပ္ၿပီး {1} AppCoins Credits ကို ရယူလိုက္ပါ။" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "{0} တြင္ သင္ဝယ္ယူမႈတစ္ခု ျပဳလုပ္ခဲ့ျခင္းေၾကာင့္ ျဖစ္ပါသည္။" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "{2} ရက္အတြင္း {0} {1} အသံုးျပဳကာ {3} AppCoins Credits မ်ားကို ရယူလိုက္ပါ။" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "လြန္ခဲ့ေသာ {2} ရက္အတြင္း သင္ {0} {1} ထက္ပို၍ အသံုးျပဳခဲ့ေသာေၾကာင့္ ျဖစ္ပါသည္။" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "ေနာက္တစ္ဆင့္သို႔ ေရာက္ေအာင္သြားကာ {0} AppCoins Credits ကို ရယူလိုက္ပါ။" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "ေနာက္တစ္ဆင့္သို႔ သင္ေရာက္ရွိခဲ့ျခင္းေၾကာင့္ ျဖစ္ပါသည္။" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "{1} တြင္ သင့္ဝယ္ယူမႈမ်ားအားလံုး၌ AppCoins Credits မွလြဲ၍ အပိုေဘာနပ္စ္ {0}% ရယူလိုက္ပါ။" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "{1} တြင္ သင့္ပထမဆံုး ဝယ္ယူမႈ၌ AppCoins Credits မွလြဲ၍ အပိုေဘာနပ္စ္ {0}% ရယူလိုက္ပါ။" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "{0} တြင္ သင့္ပထမဆံုး ဝယ္ယူမႈေၾကာင့္။" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "{1} တြင္ တစ္ေန႔တာအတြင္း သင့္ပထမဆံုး ဝယ္ယူမႈ၌ AppCoins Credits မွလြဲ၍ အပိုေဘာနပ္စ္ {0}% ရယူလိုက္ပါ" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "{0} တြင္ တစ္ေန႔တာအတြင္း သင့္ပထမဆံုး ဝယ္ယူမႈေၾကာင့္။" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} AppCoins Credits ေဘာနပ္စ္။" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "{2} တြင္ သင္ {0} {1} သံုးစြဲခဲ့ေသာေၾကာင့္။" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-nl/external_strings.xml b/app/src/main/res/external_strings/values-nl/external_strings.xml new file mode 100644 index 00000000000..b4afd353092 --- /dev/null +++ b/app/src/main/res/external_strings/values-nl/external_strings.xml @@ -0,0 +1,58 @@ + + + + Om dit item te kopen, dien je eerst naar de %s te gaan. + AppCoins Wallet + SLUITEN + + Je hebt de AppCoins Wallet nodig om deze aankoop te doen. Download de wallet via Aptoide of de Play Store en kom terug om je aankoop te voltooien! + BEGREPEN! + + Je hebt de AppCoins Wallet nodig! + Om je beloning te krijgen heb je de AppCoins Wallet nodig. + + Betaal als gast + Creditcard + PayPal + BETAAL MET PAYPAL + BETAAL MET CREDITCARD + Kaartnummer + MM/JJ + CVV + CVC/CVV + KAART WIJZIGEN + Betaal met de AppCoins Wallet + Ontvang tot %s%% bonus! + Je ontvangt %s voor deze aankoop. + Je ontvangt een bonus voor deze aankoop. + BESTE DEAL + GEREED! + Ontvang de volgende keer tot %s%% bonus door de AppCoins Wallet te gebruiken! + Ontvang de volgende keer een bonus door de AppCoins Wallet te gebruiken! + Je had %s voor deze aankoop kunnen ontvangen. + Aankoop voltooid! + MEER BETALINGSMETHODEN + Hulp nodig? + Neem contact op met support. + + VOLGENDE + ANNULEREN + KOPEN + WALLET INSTALLEREN + OK + Installeren + + Fout + Je hebt dit item al! + Oeps. Er is iets fout gegaan. + Er is een probleem met je kaart opgetreden. Probeer het opnieuw of neem contact met ons op. + De transactie is door je bank geweigerd. Gebruik een andere kaart of neem contact met ons op. + Het lijkt erop dat je niet genoeg saldo hebt of je kaart heeft een limiet. Probeer het opnieuw met een andere kaart. + Het lijkt erop dat je kaart is verlopen. Probeer het opnieuw met een andere kaart. + Weet je zeker dat je het juiste kaartnummer hebt ingevoerd? Controleer het nummer en probeer het opnieuw. + Je kaarttype wordt nog niet ondersteund. Probeer het opnieuw met een andere kaart. + Weet je zeker dat je de juiste beveiligingsinformatie hebt ingevoerd? Probeer het opnieuw. + Het lijkt erop dat de CVV/CVC-code onjuist is. Probeer het opnieuw. + Onjuiste CVV/CVC + Neem contact met ons op als het probleem zich blijft voordoen. + diff --git a/app/src/main/res/external_strings/values-nl/perks.po b/app/src/main/res/external_strings/values-nl/perks.po new file mode 100644 index 00000000000..4d340c2e2ca --- /dev/null +++ b/app/src/main/res/external_strings/values-nl/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: nl\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Dutch\n" +"Language: nl_NL\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Verdien AppCoins Credits!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Doe een aankoop in {0} en ontvang {1} AppCoins Credits." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Omdat je een aankoop in {0} hebt gedaan." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Besteed {0} {1} in {2} dagen en ontvang {3} AppCoins Credits." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Omdat je in de afgelopen {2} dagen meer dan {0} {1} hebt besteed." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Bereik het volgende niveau en ontvang {0} AppCoins Credits!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Omdat je het volgende niveau hebt bereikt." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Krijg een extra bonus van {0}% voor al je aankopen in {1}, met uitzondering van AppCoins." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Krijg een extra bonus van {0}% voor je eerste aankoop in {1}, met uitzondering van AppCoins." + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "Vanwege je eerste aankoop in {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Krijg een extra bonus van {0}% voor je eerste aankoop van de dag in {1}, met uitzondering van AppCoins." + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "Vanwege je eerste aankoop van de dag in {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+ bonus van {0} AppCoins Credits!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Omdat je {0} {1} in {2} hebt besteed." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-pa/external_strings.xml b/app/src/main/res/external_strings/values-pa/external_strings.xml new file mode 100644 index 00000000000..1b95e6e7518 --- /dev/null +++ b/app/src/main/res/external_strings/values-pa/external_strings.xml @@ -0,0 +1,58 @@ + + + + ਇਸ ਚੀਜ਼ ਨੂੰ ਖਰੀਦਣ ਲਈ ਤੁਹਾਨੂੰ ਪਹਿਲਾਂ %s ਪ੍ਰਾਪਤ ਕਰਨ ਦੀ ਲੋੜ ਹੈ| + ਐਪਸਿੱਕੇ ਬਟੂਆ + ਬੰਦ + + ਤੁਹਾਨੂੰ ਇਹ ਖਰੀਦ ਪੂਰੀ ਕਰਨ ਲਈ ਐਪ ਸਿੱਕੇ ਬਟੂਏ ਦੀ ਜ਼ਰੂਰਤ ਹੈ| ਇਸਨੂੰ ਐਪਟਾਇਡ ਜਾਂ ਪਲੇ ਸਟੋਰ ਤੋਂ ਡਾਉਨਲੋਡ ਕਰੋ ਅਤੇ ਆਪਣੀ ਖਰੀਦ ਨੂੰ ਪੂਰਾ ਕਰਨ ਲਈ ਵਾਪਸ ਆਓ! + ਠੀਕ ਹੈ! + + ਤੁਹਾਨੂੰ ਐਪ ਸਿੱਕੇ ਬਟੂਏ ਦੀ ਜ਼ਰੂਰਤ ਹੈ! + ਆਪਣਾ ਇਨਾਮ ਪ੍ਰਾਪਤ ਕਰਨ ਲਈ ਤੁਹਾਨੂੰ ਐਪ ਸਿੱਕੇ ਬਟੂਏ ਦੀ ਜ਼ਰੂਰਤ ਹੈ| + + ਮਹਿਮਾਨ ਦੇ ਤੌਰ ਤੇ ਭੁਗਤਾਨ ਕਰੋ + ਕਰੈਡਿਟ ਕਾਰਡ + ਪੇਪਾਲ + ਪੇਪਾਲ ਨਾਲ ਭੁਗਤਾਨ ਕਰੋ + ਕਰੈਡਿਟ ਕਾਰਡ ਨਾਲ ਭੁਗਤਾਨ ਕਰੋ + ਕਾਰਡ ਨੰਬਰ + ਐਮ ਐਮ / ਵਾਈ ਵਾਈ + ਸੀਵੀਵੀ + ਸੀਵੀਸੀ/ਸੀਵੀਵੀ + ਕਾਰਡ ਬਦਲੋ + ਐਪ ਸਿੱਕੇ ਬਟੂਏ ਨਾਲ ਭੁਗਤਾਨ ਕਰੋ + %s%% ਤੱਕ ਦਾ ਬੋਨਸ ਪ੍ਰਾਪਤ ਕਰੋ! + ਤੁਸੀਂ ਇਸ ਖਰੀਦ ਤੇ %s ਤੱਕ ਪ੍ਰਾਪਤ ਕਰੋਗੇ| + ਤੁਸੀਂ ਇਸ ਖਰੀਦ ਤੇ ਬੋਨਸ ਪ੍ਰਾਪਤ ਕਰੋਗੇ| + ਵਧੀਆ ਸੌਦਾ + ਹੋ ਗਿਆ! + ਅਗਲੀ ਵਾਰ, ਐਪ ਸਿੱਕੇ ਬਟੂਏ ਨਾਲ %s%% ਤੱਕ ਦਾ ਬੋਨਸ ਪ੍ਰਾਪਤ ਕਰੋ! + ਅਗਲੀ ਵਾਰ, ਐਪ ਸਿੱਕੇ ਬਟੂਏ ਨਾਲ ਬੋਨਸ ਪ੍ਰਾਪਤ ਕਰੋ! + ਤੁਸੀਂ ਇਸ ਖਰੀਦ ਤੇ %s ਤੱਕ ਪ੍ਰਾਪਤ ਕਰ ਸੱਕਦੇ ਸੀ| + ਖਰੀਦ ਪੂਰੀ ਹੋਈ! + ਭੁਗਤਾਨ ਦੇ ਹੋਰ ਤਰੀਕੇ + ਮਦਦ ਦੀ ਲੋੜ ਹੈ? + ਗਾਕ ਸਹਾਇਤਾ ਨਾਲ ਸੰਪਰਕ ਕਰੋ + + ਅਗਲਾ + ਕੈਂਸਲ + ਖਰੀਦੋ + ਬਟੂਆ ਭਰੋ + ਠੀਕ ਹੈ + ਇੰਸਟਾਲ + + ਗਲਤੀ + ਤੁਸੀਂ ਪਹਿਲਾਂ ਤੋਂ ਹੀ ਇਸ ਵਸਤੂ ਦੇ ਮਾਲਕ ਹੋ! + ਓਹ ਹੋ, ਕੁਝ ਗ਼ਲਤ ਹੋਇਆ| + ਤੁਹਾਡੇ ਕਾਰਡ ਨਾਲ ਕੁੱਝ ਸਮੱਸਿਆ ਸੀ| ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜਾਂ ਸਾਡੇ ਨਾਲ ਸੰਪਰਕ ਕਰੋ| + ਭੁਗਤਾਨ ਨੂੰ ਤੁਹਾਡੇ ਬੈਂਕ ਦੁਆਰਾ ਰੱਦ ਕਰ ਦਿੱਤਾ ਗਿਆ ਹੈ| ਕਿਰਪਾ ਕਰਕੇ ਵੱਖਰੇ ਕਾਰਡ ਨਾਲ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜਾਂ ਸਾਡੇ ਨਾਲ ਸੰਪਰਕ ਕਰੋ| + ਅਜਿਹਾ ਲਗਦਾ ਹੈ ਕਿ ਤੁਹਾਡੇ ਕੋਲ ਲੋੜੀਂਦੇ ਪੈਸੇ ਨਹੀਂ ਹਨ ਜਾਂ ਤੁਹਾਡੇ ਕਾਰਡ ਦੀ ਭੁਗਤਾਨ ਦੀ ਕੋਈ ਸੀਮਾ ਹੈ| ਕਿਰਪਾ ਕਰਕੇ ਕਿਸੇ ਵੱਖਰੇ ਕਾਰਡ ਨਾਲ ਕੋਸ਼ਿਸ਼ ਕਰੋ| + ਅਜਿਹਾ ਲਗਦਾ ਹੈ ਕਿ ਤੁਹਾਡੇ ਕਾਰਡ ਦੀ ਮਿਆਦ ਖਤਮ ਹੋ ਗਈ ਹੈ| ਕਿਰਪਾ ਕਰਕੇ ਕਿਸੇ ਵੱਖਰੇ ਕਾਰਡ ਨਾਲ ਕੋਸ਼ਿਸ਼ ਕਰੋ| + ਕੀ ਤੁਹਾਨੂੰ ਯਕੀਨ ਹੈ ਕਿ ਤੁਹਾਡਾ ਕਾਰਡ ਨੰਬਰ ਸਹੀ ਹੈ? ਕਿਰਪਾ ਕਰਕੇ ਜਾਂਚ ਕਰੋ ਅਤੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ| + ਤੁਹਾਡੇ ਕਾਰਡ ਦੀ ਕਿਸਮ ਅਜੇ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ| ਕਿਸੇ ਵੱਖਰੇ ਕਾਰਡ ਨਾਲ ਕੋਸ਼ਿਸ਼ ਕਰੋ| + ਕੀ ਤੁਹਾਨੂੰ ਯਕੀਨ ਹੈ ਕਿ ਸੁਰੱਖਿਆ ਜਾਣਕਾਰੀ ਸਹੀ ਹੈ? ਮੁੜ ਕੋਸ਼ਿਸ ਕਰੋ ਜੀ| + ਤੁਹਾਡਾ ਸੀਵੀਵੀ/ਸੀਵੀਸੀ ਕੋਡ ਗਲਤ ਜਾਪਦਾ ਹੈ| ਮੁੜ ਕੋਸ਼ਿਸ ਕਰੋ ਜੀ| + ਗਲਤ ਸੀਵੀਵੀ/ਸੀਵੀਸੀ + ਜੇ ਸਮੱਸਿਆ ਬਣੀ ਰਹਿੰਦੀ ਹੈ ਤਾਂ ਕਿਰਪਾ ਕਰਕੇ ਸਾਡੇ ਨਾਲ ਸੰਪਰਕ ਕਰੋ| + diff --git a/app/src/main/res/external_strings/values-pa/perks.po b/app/src/main/res/external_strings/values-pa/perks.po new file mode 100644 index 00000000000..4b6a1cfd31b --- /dev/null +++ b/app/src/main/res/external_strings/values-pa/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: pa-IN\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Punjabi\n" +"Language: pa_IN\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "ਐਪ ਸਿੱਕੇ ਕ੍ਰੈਡਿਟ ਕਮਾਓ!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "{0} ਵਿਚ ਖਰੀਦੋ ਅਤੇ {1} ਐਪ ਸਿੱਕੇ ਕ੍ਰੈਡਿਟ ਪ੍ਰਾਪਤ ਕਰੋ|" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "ਕਿਉਂਕਿ ਤੁਸੀਂ {0} ਵਿੱਚ ਇੱਕ ਖਰੀਦ ਕੀਤੀ|" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "{2} ਦਿਨਾਂ ਵਿੱਚ {0} {1} ਖਰਚ ਕਰੋ ਅਤੇ {3} ਐਪ ਸਿੱਕੇ ਕ੍ਰੈਡਿਟ ਪ੍ਰਾਪਤ ਕਰੋ" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "ਕਿਉਂਕਿ ਤੁਸੀਂ ਆਖਰੀ {2} ਦਿਨਾਂ ਵਿੱਚ {0} {1} ਤੋਂ ਵੱਧ ਖਰਚ ਕੀਤਾ" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "ਅਗਲੇ ਪੱਧਰ ਤੇ ਪਹੁੰਚੋ ਅਤੇ {0} ਐਪ ਸਿੱਕੇ ਕ੍ਰੈਡਿਟ ਪ੍ਰਾਪਤ ਕਰੋ!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "ਕਿਉਂਕਿ ਤੁਸੀਂ ਅਗਲੇ ਪੱਧਰ ਤੇ ਪਹੁੰਚ ਗਏ|" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "ਐਪ ਸਿੱਕੇ ਕ੍ਰੈਡਿਟ ਨੂੰ ਛੱਡ ਕੇ, {1} ਵਿੱਚ, ਤੁਹਾਡੀਆਂ ਸਾਰੀਆਂ ਖਰੀਦਾਂ ਤੇ ਇੱਕ ਵਾਧੂ {0}% ਬੋਨਸ ਪ੍ਰਾਪਤ ਕਰੋ|" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "ਐਪ ਸਿੱਕੇ ਕ੍ਰੈਡਿਟ ਨੂੰ ਛੱਡ ਕੇ, {1} ਵਿੱਚ, ਆਪਣੀ ਪਹਿਲੀ ਖਰੀਦ ਤੇ ਇੱਕ ਵਾਧੂ {0}% ਬੋਨਸ ਪ੍ਰਾਪਤ ਕਰੋ|" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "{0} ਵਿੱਚ ਤੁਹਾਡੀ ਪਹਿਲੀ ਖਰੀਦ ਕਰਕੇ|" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "{1} ਵਿੱਚ, ਐਪ ਸਿੱਕੇ ਕ੍ਰੈਡਿਟ ਨੂੰ ਛੱਡ ਕੇ, , ਆਪਣੀ ਦਿਨ ਦੀ ਪਹਿਲੀ ਖਰੀਦ ਤੇ ਇੱਕ ਵਾਧੂ {0}% ਬੋਨਸ ਪ੍ਰਾਪਤ ਕਰੋ|" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "{0} ਵਿੱਚ ਤੁਹਾਡੀ ਦਿਨ ਦੀ ਪਹਿਲੀ ਖਰੀਦ ਕਰਕੇ|" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} ਐਪ ਸਿੱਕੇ ਕ੍ਰੈਡਿਟ ਬੋਨਸ!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "ਕਿਉਂਕਿ ਤੁਸੀਂ {0} {1} ਨੂੰ {2} ਵਿੱਚ ਖਰਚਿਆ|" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-pl/external_strings.xml b/app/src/main/res/external_strings/values-pl/external_strings.xml new file mode 100644 index 00000000000..7f92c7735a0 --- /dev/null +++ b/app/src/main/res/external_strings/values-pl/external_strings.xml @@ -0,0 +1,58 @@ + + + + Aby kupić ten przedmiot, najpierw musisz mieć %s. + Portfel AppCoins + ZAMKNIJ + + You need the AppCoins Wallet to make this purchase. Download it from Aptoide or Play Store and come back to complete your purchase! + GOT IT! + + Potrzebujesz portfela AppCoins! + Aby odebrać nagrodę, potrzebujesz portfela AppCoins. + + Pay as a guest + Credit Card + PayPal + PAY USING PAYPAL + PAY USING CREDIT CARD + Card number + MM/YY + CVV + CVC/CVV + ZMIANA KARTY + Pay with AppCoins Wallet + Get up to %s%% Bonus! + You\'ll receive %s on this purchase. + You\'ll receive a Bonus on this purchase. + BEST DEAL + DONE! + Next time, get up to %s%% Bonus with the AppCoins Wallet! + Next time, get a Bonus with the AppCoins Wallet! + You could have received %s on this purchase. + Purchase completed! + MORE PAYMENT METHODS + Need help? + Contact Support. + + NEXT + Anuluj + BUY + ZAINSTALUJ PORTFEL + Ok + Install + + Błąd + You already own this item! + Oops, something went wrong. + There was a problem with your card. Please try again or contact us. + The transaction has been rejected by your bank. Please try with a different card or contact us. + It seems you don\'t have enough funds or there\'s a limit on your card. Please try with a different one. + It seems your card has expired. Please try with a different one. + Are you sure your card number is correct? Please check and try again. + Your card type is not supported yet. Try with a different one. + Are you sure the security information is correct? Please try again. + Your CVV/CVC code seems to be wrong. Please try again. + Wrong CVV/CVC + If the problem persists please contact us. + diff --git a/app/src/main/res/external_strings/values-pl/perks.po b/app/src/main/res/external_strings/values-pl/perks.po new file mode 100644 index 00000000000..47e158aa68f --- /dev/null +++ b/app/src/main/res/external_strings/values-pl/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: pl\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Polish\n" +"Language: pl_PL\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-pt-rBR/external_strings.xml b/app/src/main/res/external_strings/values-pt-rBR/external_strings.xml new file mode 100644 index 00000000000..f07ddaa8e91 --- /dev/null +++ b/app/src/main/res/external_strings/values-pt-rBR/external_strings.xml @@ -0,0 +1,58 @@ + + + + Para comprar este item, você primeiro precisa obter a %s. + AppCoins Wallet + FECHAR + + Você precisa da AppCoins Wallet para fazer esta compra. Faça o download na Aptoide ou na Play Store e retorne para completar sua compra! + ENTENDI! + + Você precisa da AppCoins Wallet! + Para receber recompensas, você precisa da AppCoins Wallet. + + Pagar como convidado + Cartão de Crédito + PayPal + PAGAR COM O PAYPAL + PAGAR COM CARTÃO DE CRÉDITO + Número do cartão + MM/AA + CVV + CVC/CVV + ALTERAR CARTÃO + Pagar com a AppCoins Wallet + Receba até %s%% de Bônus! + Você receberá %s nesta compra. + Você receberá um Bônus nesta compra. + MELHOR NEGÓCIO + PRONTO! + Na próxima vez, receba até %s%% de Bônus com a AppCoins Wallet! + Na próxima vez, ganhe um Bônus com a AppCoins Wallet! + Você deixou de receber %s nesta compra. + Compra concluída! + OUTROS MÉTODOS DE PAGAMENTO + Precisa ajuda? + Fale com o suporte. + + PRÓXIMO + CANCELAR + COMPRAR + INSTALAR WALLET + OK + Instalar + + Erro + Você já possui este item! + Ops, algo deu errado. + Ocorreu um problema com o seu cartão. Por favor, tente novamente ou entre em contato conosco. + A transação foi rejeitada pelo seu banco. Tente com um cartão diferente ou entre em contato. + Parece que você não tem saldo suficiente ou há um limite no seu cartão. Por favor, tente com um cartão diferente. + Parece que seu cartão expirou. Por favor, tente com um diferente. + Tem certeza de que o número do seu cartão está correto? Por favor verifique e tente novamente. + O seu tipo de cartão ainda não é aceito. Tente com um diferente. + Tem certeza de que as informações de segurança estão corretas? Por favor, tente novamente. + Seu código CVV/CVC parece estar errado. Por favor, tente novamente. + CVV/CVC errado + Se o problema persistir, entre em contato conosco. + diff --git a/app/src/main/res/external_strings/values-pt-rBR/perks.po b/app/src/main/res/external_strings/values-pt-rBR/perks.po new file mode 100644 index 00000000000..dbb6d614ef1 --- /dev/null +++ b/app/src/main/res/external_strings/values-pt-rBR/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: pt-BR\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Portuguese, Brazilian\n" +"Language: pt_BR\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Ganhe Créditos AppCoins!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Faça uma compra em {0} e receba {1} em Créditos AppCoins." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Porque você fez uma compra em {0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Gaste {0} {1} em {2} dias e receba {3} em Créditos AppCoins." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Porque você gastou mais de {0} {1} nos últimos {2} dias." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Alcance o próximo nível e receba {0} Créditos AppCoins!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Porque você passou de nível." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Receba um Bônus extra de {0}% em todas as suas compras, exceto Créditos AppCoins em {1}." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Receba um Bônus extra de {0}% na sua primeira compra, exceto Créditos AppCoins em {1}" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "Porque você fez sua primeira compra em {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Receba um Bônus extra de {0}% em sua primeira compra do dia, exceto Créditos AppCoins em {1}" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "Porque você fez sua primeira compra do dia em {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} Bônus em Créditos AppCoins!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Porque você gastou {0} {1} em {2}." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-pt/external_strings.xml b/app/src/main/res/external_strings/values-pt/external_strings.xml new file mode 100644 index 00000000000..17cf62369fa --- /dev/null +++ b/app/src/main/res/external_strings/values-pt/external_strings.xml @@ -0,0 +1,58 @@ + + + + Para comprares este item, tens que primeiro ter a %s. + AppCoins Wallet + FECHAR + + Precisas da AppCoins Wallet para fazer esta compra. Faz o download na Aptoide ou Play Store e volta para concluíres a tua compra! + JÁ PERCEBI! + + Precisas da AppCoins Wallet! + Para receberes o teu prémio, precisas da AppCoins Wallet. + + Pagar como convidado + Cartão de Crédito + PayPal + PAGAR COM O PAYPAL + PAGAR COM CARTÃO DE CRÉDITO + Número do cartão + MM/AA + CVV + CVC/CVV + ALTERAR CARTÃO + Pagar com a AppCoins Wallet + Recebe até %s%% de Bónus! + Vais receber %s nesta compra. + Vais receber um Bónus nesta compra. + MELHOR NEGÓCIO + FEITO! + Da próxima vez ganha até %s%% de Bónus com a AppCoins Wallet! + Da próxima vez recebe um Bónus com a AppCoins Wallet! + Podias ter recebido %s nesta compra. + Compra concluída! + MAIS MÉTODOS DE PAGAMENTO + Precisas de ajuda? + Contacta a Equipa Apoio. + + PRÓXIMO + CANCELAR + COMPRAR + INSTALAR WALLET + OK + Instalar + + Erro + Já tens este item! + Ups, alguma coisa correu mal. + Houve um problema com o teu cartão. Por favor tenta novamente ou entra em contato connosco. + A transação foi rejeitada pelo teu banco. Por favor tenta com um cartão diferente ou entra em contacto connosco. + Parece que não tens saldo suficiente ou há um limite no teu cartão. Por favor tenta com um diferente. + Parece que o teu cartão caducou. Por favor tenta com um diferente. + Tens a certeza de que o número do teu cartão está correto? Por favor verifica e tenta novamente. + O teu tipo de cartão ainda não é suportado. Tenta com um diferente. + Tens a certeza de que a informação de segurança está correta? Por favor tenta novamente. + O teu código CVV/CVC parece estar errado. Por favor tenta novamente. + CVV/CVC errado + Se o problema persistir entra em contacto connosco. + diff --git a/app/src/main/res/external_strings/values-pt/perks.po b/app/src/main/res/external_strings/values-pt/perks.po new file mode 100644 index 00000000000..ef56c3dc6cb --- /dev/null +++ b/app/src/main/res/external_strings/values-pt/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: pt-PT\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Portuguese\n" +"Language: pt_PT\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Ganha AppCoins Credits!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Faz uma compra em {0} e recebe {1} AppCoins Credits." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Porque fizeste uma compra em {0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Gasta {0} {1} em {2} dias e recebe {3} AppCoins Credits." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Porque gastaste mais de {0} {1} nos últimos {2} dias." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Chega ao próximo nível e recebe {0} AppCoins Credits!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Porque chegaste ao próximo nível." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Recebe um Bónus extra de {0}% em todas as tuas compras, exceto AppCoins Credits, em {1}." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Recebe um Bónus extra de {0}% na tua primeira compra, exceto AppCoins Credits, em {1}" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "Pela tua primeira compra em {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Recebe um Bónus extra de {0}% na tua primeira compra do dia, exceto AppCoins Credits, em {1}" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "Pela tua primeira compra do dia em {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "Bonus de +{0} AppCoins Credits!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Porque gastaste {0} {1} em {2}." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-ro/external_strings.xml b/app/src/main/res/external_strings/values-ro/external_strings.xml new file mode 100644 index 00000000000..9a3bfc87a2b --- /dev/null +++ b/app/src/main/res/external_strings/values-ro/external_strings.xml @@ -0,0 +1,58 @@ + + + + Pentru a cumpăra acest articol trebuie să descarci %s. + AppCoins Wallet + ÎNCHIDE + + Ai nevoie de AppCoins Wallet pentru a face această cumpărătură. Descarcă-l din Aptoide sau din Play Store și întoarce-te pentru a finaliza cumpărătura! + AM ÎNȚELES! + + Ai nevoie de AppCoins Wallet! + Pentru a-ți primi recompensa ai nevoie de AppCoins Wallet. + + Plătește ca vizitator + Card de credit + PayPal + PLĂTEȘTE CU PAYPAL + PLĂTEȘTE CU CARD DE CREDIT + Număr card + LL/AA + CVV + CVC/CVV + SCHIMBĂ CARD + Plătește cu AppCoins Wallet + Primești un Bonus de până la %s%%! + Vei primi %s pentru această cumpărătură. + Vei primi un Bonus pentru această cumpărătură. + CEA MAI BUNĂ OFERTĂ + GATA! + Data viitoare, primește un Bonus de până la %s%% cu AppCoins Wallet! + Data viitoare, primește un Bonus cu AppCoins Wallet! + Ai fi putut să primești %s pentru această cumpărătură. + Cumpărătură finalizată! + MAI MULTE METODE DE PLATĂ + Ai nevoie de ajutor? + Contactează echipa de asistență. + + URMĂTORUL + ANULEAZĂ + CUMPĂRĂ + INSTALEAZĂ WALLET + OK + Instalează + + Eroare + Ai deja acest articol! + Ups, ceva nu a mers cum trebuie. + A apărut o problemă cu cardul tău. Încearcă din nou sau contactează-ne. + Tranzacția a fost refuzată de banca ta. Încearcă cu un alt card sau contactează-ne. + Se pare că nu ai fonduri suficiente sau există o limită pentru cardul tău. Încearcă cu un alt card. + Se pare că ai folosit un card care a expirat. Încearcă cu un alt card. + Ești sigur că numărul cardului este corect? Verifică și încearcă din nou. + Tipul tău de card nu este încă acceptat. Încearcă cu un alt card. + Ești sigur că informațiile de securitate sunt corecte? Încearcă din nou. + Codul tău CVV/CVC pare a fi greșit. Încearcă din nou. + CVV/CVC greșit + Dacă problema continuă, te rugăm să ne contactezi. + diff --git a/app/src/main/res/external_strings/values-ro/perks.po b/app/src/main/res/external_strings/values-ro/perks.po new file mode 100644 index 00000000000..3d0f944db82 --- /dev/null +++ b/app/src/main/res/external_strings/values-ro/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: ro\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Romanian\n" +"Language: ro_RO\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Câștigă AppCoins Credits!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Fă o cumpărătură în {0} și primești {1} AppCoins Credits." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Pentru că ai făcut o cumpărătură în {0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Cheltuiește {0} {1} în {2} zile și primești {3} AppCoins Credits." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Pentru că ai cheltuit mai mult de {0} {1} în ultimele {2} zile." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Urcă la nivelul următor și primești {0} AppCoins Credits!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Pentru că ai ajuns la nivelul următor." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Primești un Bonus suplimentar de {0}% la toate cumpărăturile tale, cu excepția AppCoins Credits, în {1}." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Primești un Bonus suplimentar de {0}% la prima ta cumpărătură, cu excepția AppCoins Credits, în {1}" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "Pentru că ai făcut prima ta cumpărătură în {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Primești un Bonus suplimentar de {0}% la prima cumpărătură a zilei, cu excepția AppCoins Credits, în {1}" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "Pentru că ai făcut prima cumpărătură a zilei în {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} Bonus AppCoins Credits!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Pentru că ai cheltuit {0} {1} în {2}." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-ru/external_strings.xml b/app/src/main/res/external_strings/values-ru/external_strings.xml new file mode 100644 index 00000000000..33f3736db71 --- /dev/null +++ b/app/src/main/res/external_strings/values-ru/external_strings.xml @@ -0,0 +1,58 @@ + + + + Чтобы купить этот предмет, сначала нужно получить %s. + AppCoins Wallet + ЗАКРЫТЬ + + Вам нужен AppCoins Wallet, чтобы совершить эту покупку. Загрузите его с Aptoide или Play Store, и возвращайтесь, чтобы завершить вашу покупку! + ЯСНО! + + Вам необходим AppCoins Wallet! + Чтобы получить вашу награду, вам нужен AppCoins Wallet. + + Оплатить как гость + Кредитной картой + PayPal + ОПЛАТИТЬ С PAYPAL + ОПЛАТИТЬ КРЕДИТНОЙ КАРТОЙ + Номер карты + ММ/ГГ + CVV + CVC/CVV + ИЗМЕНИТЬ КАРТУ + Оплатить с AppCoins Wallet + Получите бонус до %s%%! + Вы получите %s на эту покупку. + Вы получите бонус за эу покупку. + ЛУЧШАЯ СДЕЛКА + СДЕЛАНО! + В следующий раз получите бонус до %s%% с AppCoins Wallet! + В следующий раз получите бонус с AppCoins Wallet! + Вы могли бы получить %s на эту покупку. + Покупка завершена! + ДРУГИЕ МЕТОДЫ ОПЛАТЫ + Нужна помощь? + Свяжитесь с поддержкой. + + ДАЛЕЕ + ОТМЕНИТЬ + КУПИТЬ + УСТАНОВИТЬ КОШЕЛЁК + ОК + Установить + + Ошибка + У вас уже есть этот товар! + Ой, что-то пошло не так. + С вашей картой возникла проблема. Пожалуйста, попробуйте снова, или свяжитесь с нами. + Ваш банк отказал в данной транзакции. Пожалуйста, попробуйте другую карту, или свяжитесь с нами. + Кажется, у вас недостаточно средств, или для карты установлен лимит. Пожалуйста, попробуйте другую. + Кажется ваша карта истекла. Пожалуйста, попробуйте другую. + Вы уверены, что правильно ввели номер карты? Пожалуйста, проверьте и попробуйте снова. + Тип вашей карты пока не поддерживается. Попробуйте другую карту. + Вы уверены, что правильно ввели атрибуты безопасности? Пожалуйста, попробуйте снова. + Кажеся, вы ввели неверный CVV/CVC код. Пожалуйста, попробуйте снова. + Неверный CVV/CVC + Если проблема не исчезнет – пожалуйста, свяжитесь с нами. + diff --git a/app/src/main/res/external_strings/values-ru/perks.po b/app/src/main/res/external_strings/values-ru/perks.po new file mode 100644 index 00000000000..e650cfb16bc --- /dev/null +++ b/app/src/main/res/external_strings/values-ru/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: ru\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Russian\n" +"Language: ru_RU\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Заработать AppCoins Credits!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Сделайте покупку в {0} и получите {1} AppCoins Credits." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Потому что вы сделали покупку в {0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Потратьте {0} {1} за {2} дней и получите {3} AppCoins Credits." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Потому что вы потратили {0} {1} за последние {2} дней." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Получите следующий уровень и получите {0} AppCoins Credits!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Потому что вы достигли следующего уровня." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Получите бонус в {0}% на все ваши покупки в {1}, кроме покупок AppCoins Credits." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Получите бонус в {0}% на вашу первую покупку в {1}, кроме покупок AppCoins Credits" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "За вашу первую покупку в {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Получите бонус в {0}% на вашу первую покупку за день в {1}, кроме покупок AppCoins Credits" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "За вашу первую покупку за день в {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "Бонус +{0} AppCoins Credits!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Потому, что вы потратили {0} {1} в {2}." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-sr/external_strings.xml b/app/src/main/res/external_strings/values-sr/external_strings.xml new file mode 100644 index 00000000000..46f06fc6d35 --- /dev/null +++ b/app/src/main/res/external_strings/values-sr/external_strings.xml @@ -0,0 +1,58 @@ + + + + Da biste kupili ovaj artikal, prvo morate da nabavite %s. + AppCoins Wallet + ZATVORI + + Da biste obavili ovu kupovinu treba vam AppCoins Wallet. Preuzmite ga iz aplikacije Aptoide ili Play prodavnice i vratite se da dovršite kupovinu! + RAZUMEM! + + Treba vam AppCoins Wallet! + Da biste dobili nagradu, treba vam AppCoins Wallet. + + Platite kao gost + Kreditna kartica + PayPal + PLATITE POMOĆU APLIKACIJE PAYPAL + PLATITE POMOĆU KREDITNE KARTICE + Broj kartice + MM/GG + CVV + CVC/CVV + PROMENITE KARTICU + Platite pomoću aplikacije AppCoins Wallet + Ostvarite do %s%% bonusa! + Dobićete %s za ovu kupovinu. + Dobićete bonus za ovu kupovinu. + NAJBOLJA PONUDA + GOTOVO! + Sledeći put ostvarite do %s%% bonusa pomoću aplikacije AppCoins Wallet! + Sledeći put ostvarite bonus pomoću aplikacije AppCoins Wallet! + Mogli ste da dobijete %s za ovu kupovinu. + Kupovina je dovršena! + JOŠ NAČINA PLAĆANJA + Potrebna vam je pomoć? + Obratite se podršci. + + SLEDEĆE + OTKAŽI + KUPI + INSTALIRAJ NOVČANIK + U redu + Instaliraj + + Greška + Već imate ovaj artikal! + Ups, došlo je do greške. + Došlo je do problema sa vašom karticom. Pokušajte ponovo ili nam se obratite. + Vaša banka je odbila transakciju. Pokušajte sa drugom karticom ili nam se obratite. + Izgleda da nemate dovoljno sredstava na kartici ili da postoji limit. Pokušajte sa drugom karticom. + Izgleda da je vaša kartica istekla. Pokušajte sa drugom karticom. + Da li ste sigurni da je broj kartice tačan? Proverite pa pokušajte ponovo. + Tip vaše kartice još nije podržan. Pokušajte sa drugom karticom. + Da li ste sigurni da su bezbednosne informacije tačne? Pokušajte ponovo. + Izgleda da je CVV/CVC kod pogrešan. Pokušajte ponovo. + Pogrešan CVV/CVC + Ako se problem i dalje javlja, obratite nam se. + diff --git a/app/src/main/res/external_strings/values-sr/perks.po b/app/src/main/res/external_strings/values-sr/perks.po new file mode 100644 index 00000000000..2734a490da1 --- /dev/null +++ b/app/src/main/res/external_strings/values-sr/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: sr-CS\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Serbian (Latin)\n" +"Language: sr_CS\n" +"PO-Revision-Date: 2020-10-15 14:27\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Zaradite AppCoins Credits!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Kupite nešto u aplikaciji {0} i dobićete {1} AppCoins Credits." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Zato što ste obavili kupovinu u aplikaciji {0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Potrošite {0} {1} u roku od {2} dana i dobićete {3} AppCoins Credits." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Zato što ste potrošili više od {0} {1} u prethodnom periodu od {2} dana." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Pređite na sledeći nivo i dobićete {0} AppCoins Credits!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Zato što ste dostigli sledeći nivo." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Dobićete dodatni bonus od {0}% za sve kupovine, osim kupovine pomoću AppCoins Credits, u igri {1}." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Dobićete dodatni bonus od {0}% za prvu kupovinu, osim kupovine pomoću AppCoins Credits, u igri {1}" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "Zbog vaše prve kupovine u aplikaciji {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Dobićete dodatni bonus od {0}% za prvu kupovinu tokom dana, osim kupovine pomoću AppCoins Credits, u igri {1}" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "Zbog vaše prve kupovine tokom dana u aplikaciji {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} AppCoins Credits bonusa!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Zato što ste potrošili {0} {1} u aplikaciji {2}." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-th/external_strings.xml b/app/src/main/res/external_strings/values-th/external_strings.xml new file mode 100644 index 00000000000..97bb1fe5df7 --- /dev/null +++ b/app/src/main/res/external_strings/values-th/external_strings.xml @@ -0,0 +1,58 @@ + + + + เพื่อซื้อสินค้าชิ้นนี้คุณจำเป็นต้องมี %s ก่อน + กระเป๋าสตางค์ AppCoins + ปิด + + คุณต้องใช้ AppCoins Wallet เพื่อทำการสั่งซื้อนี้ ดาวน์โหลดจาก Aptoide หรือ Play Store และกลับมาซื้อให้เสร็จ + เข้าใจล่ะ! + + คุณจำเป็นต้องมีกระเป๋าสตางค์ AppCoins + คุณจำเป็นต้องมีกระเป๋าสตางค์ AppCoins เพื่อรับรางวัลของคุณ + + ชำระในฐานะแขก + บัตรเครดิต + Paypal + ชำระด้วย Paypal + ชำระด้วยบัตรเครดิต + หมายเลขบัตร + ดด/ปป + CVV + CVC/CVV + เปลี่ยนบัตร + ชำระด้วย AppCoins Wallet + รับโบนัสสูงสุด %s%%! + คุณจะได้รับ %s ในการซื้อครั้งนี้ + คุณจะได้รับโบนัส %s ในการซื้อครั้งนี้ + ข้อเสนอสุดพิเศษ + สำเร็จ! + ครั้งต่อไปรับโบนัสสูงสุด %s%% ด้วย AppCoins Wallet! + ครั้งต่อไปรับโบนัสสูงสุด ด้วย AppCoins Wallet! + คุณอาจได้รับ %s จากการสั่งซื้อนี้ + การซื้อสำเร็จ! + วิธีการชำระเพิ่มเติม + ต้องการความช่วยเหลือ? + ติดต่อฝ่ายสนับสนุน + + ต่อไป + ยกเลิก + ซื้อ + ติดตั้งกระเป๋าสตางค์ + ตกลง + ติดตั้ง + + ข้อผิดพลาด + คุณเป็นเจ้าของชิ้นนี้แล้ว! + อุ๊ปส์ มีบางอย่างผิดพลาด + บัตรของคุณมีอัญหา โปรดลองใหม่อีกครั้งหรือติดต่อเรา + การทำธุรกรรมถูกปฏิเสธโดยธนาคารของคุณ โปรดลองบัตรใบอื่นหรือติดต่อเรา + ดูเหมือนว่าคุณไม่มีเงินเพียงพอหรือบัตรถูกจำกัด โปรดลองบัตรใบอื่น + ดูเหมือนว่าบัตรของคุณหมดอายุ โปรดลองบัตรใบอื่น + คุณมั่นใจว่าหมายเลขบัตรถูกต้องใช่ไหม? โปรดตรวจสอบและลองใหม่อีกครั้ง + ประเภทบัตรของคุณยังไม่ได้รับการรับรอง โปรดลองบัตรใบอื่น + คุณมั่นใจว่าข้อมูลความปลอดภัยถูกต้องใช่ไหม? โปรดลองใหม่อีกครั้ง + รหัส CVV / CVC ของคุณดูเหมือนจะผิด กรุณาลองอีกครั้ง + CVV/CVC ไม่ถูกต้อง + โปรดติดต่อเราหากยังคงมีปัญหา + diff --git a/app/src/main/res/external_strings/values-th/perks.po b/app/src/main/res/external_strings/values-th/perks.po new file mode 100644 index 00000000000..bdcd14e81d7 --- /dev/null +++ b/app/src/main/res/external_strings/values-th/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: th\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Thai\n" +"Language: th_TH\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "รับเครดิต AppCoins!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "ดำเนินการซื้อใน {0} และรับ {1} AppCoins Credits" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "เนื่องจากคุณทำการซื้อใน {0}" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "ใช้จ่าย {0} {1} ใน {2} วันและรับ {3} AppCoins Credits" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "เนื่องจากคุณใช้จ่ายมากกว่า {0} {1} ในช่วง {2} วันที่ผ่านมา" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "ไปถึงระดับถัดไปและรับ {0} AppCoins Credits!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "เพราะคุณถึงเลเวลถัดไป" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "รับโบนัสพิเศษ {0}% สำหรับการซื้อทั้งหมดของคุณ ยกเว้นเครดิต AppCoins ใน {1}" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "รับโบนัสพิเศษ {0}% สำหรับการซื้อครั้งแรกของคุณ ยกเว้นเครดิต AppCoins ใน {1}" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "เนื่องจากการซื้อครั้งแรกของคุณใน {0}" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "รับโบนัสพิเศษ {0}% ในการซื้อครั้งแรกของวัน ยกเว้น AppCoins Credits ใน {1}" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "เนื่องจากการซื้อครั้งแรกในวันของคุณใน {0}" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0}โบนัส AppCoins Credits!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "เนื่องจากคุณใช้จ่ายไป {0} {1} ใน {2}" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-tr/external_strings.xml b/app/src/main/res/external_strings/values-tr/external_strings.xml new file mode 100644 index 00000000000..bc0ffe6c70d --- /dev/null +++ b/app/src/main/res/external_strings/values-tr/external_strings.xml @@ -0,0 +1,58 @@ + + + + Bu ürünü satın almak için öncelikle %s edinmeniz gerekiyor. + AppCoins Wallet + KAPAT + + Bu satın alma işlemini gerçekleştirmek için AppCoins Wallet\'a ihtiyacınız var. Aptoide veya Play Store\'dan indirin ve satın alma işleminizi tamamlamak için tekrar buraya dönün! + ANLADIM! + + AppCoins Wallet\'a ihtiyacınız var! + Ödülünüzü almak için AppCoins Wallet\'a ihtiyacınız var. + + Misafir olarak oyna + Kredi Kartı + PayPal + PAYPAL\'LE ÖDE + KREDİ KARTIYLA ÖDE + Kart numarası + AA/YY + CVV + CVC/CVV + KARTI DEĞİŞTİR + AppCoins Wallet\'la öde + %%%s oranına varan Bonus kazanın! + Bu satın alma işleminden %s alacaksınız. + Bu satın alma işleminden Bonus alacaksınız. + EN İYİ FIRSAT + BİTTİ! + Bir dahaki sefere, AppCoins Wallet\'la %%%s oranına varan Bonus kazanın! + Bir dahaki sefere, AppCoins Wallet\'la Bonus kazanın! + Bu satın alma işleminden %s alabilirdiniz. + Satın alma işlemi tamamlandı! + DAHA FAZLA ÖDEME YÖNTEMİ + Yardım mı gerekiyor? + Destekle İletişime Geçin. + + İLERİ + İPTAL ET + SATIN AL + CÜZDANI YÜKLE + Tamam + Yükle + + Hata + Bu öğeye zaten sahipsiniz! + Hay aksi, bir şeyler ters gitti. + Kartınızla ilgili bir sorun vardı. Lütfen tekrar deneyin veya bizimle iletişime geçin. + İşlem bankanız tarafından reddedildi. Lütfen farklı bir kartla deneyin veya bizimle iletişime geçin. + Bakiyeniz yetersiz veya kartınızda limit var gibi görünüyor. Lütfen farklı bir kartla deneyin. + Kartınızın son kullanma tarihi geçmiş gibi görünüyor. Lütfen farklı bir kartla deneyin. + Kart numaranızın doğru olduğundan emin misiniz? Lütfen kontrol edip tekrar deneyin. + Kart türünüz henüz desteklenmiyor. Farklı bir kartla deneyin. + Güvenlik bilgilerinizin doğru olduğundan emin misiniz? Lütfen tekrar deneyin. + CVV/CVC kodunuz yanlış gibi görünüyor. Lütfen tekrar deneyin. + Yanlış CVV/CVC + Sorun devam ederse lütfen bizimle iletişime geçin. + diff --git a/app/src/main/res/external_strings/values-tr/perks.po b/app/src/main/res/external_strings/values-tr/perks.po new file mode 100644 index 00000000000..7a71d07f002 --- /dev/null +++ b/app/src/main/res/external_strings/values-tr/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: tr\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Turkish\n" +"Language: tr_TR\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "AppCoins Credits kazanın!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "{0} oyununda bir satın alma işlemi gerçekleştirin ve {1} AppCoins Credits kazanın." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "{0} uygulamasında bir satın alma işlemi gerçekleştirdiğiniz için." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "{2} gün içinde {0} {1} harcayın ve {3} AppCoins Credits kazanın." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Son {2} günde {0} {1} üzerinde bir tutar harcadığınız için." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Bir sonraki seviyeye ulaşın ve {0} AppCoins Credits kazanın!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Bir sonraki seviyeye ulaştığınız için." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "AppCoins Credits dışında, {1} oyunundaki tüm satın alma işlemlerinizde ekstra %{0} Bonus kazanın." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "AppCoins Credits dışında, {1} oyunundaki ilk satın alma işleminizde ekstra %{0} Bonus kazanın" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "{0} uygulamasındaki ilk satın alma işleminiz için." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "AppCoins Credits dışında, {1} oyunundaki günün ilk satın alma işleminizde ekstra %{0} Bonus kazanın" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "{0} uygulamasındaki günün ilk satın alma işleminiz için." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} AppCoins Credits Bonusu!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "{2} uygulamasında {0} {1} harcadığınız için." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-vi/external_strings.xml b/app/src/main/res/external_strings/values-vi/external_strings.xml new file mode 100644 index 00000000000..babb2bf2251 --- /dev/null +++ b/app/src/main/res/external_strings/values-vi/external_strings.xml @@ -0,0 +1,58 @@ + + + + Để mua vật phẩm này, trước tiên bạn cần tải %s. + AppCoins Wallet + ĐÓNG + + Bạn cần Ví AppCoins để thực hiện giao dịch này. Hãy tải về từ Aptoide hoặc Play Store và quay lại để hoàn tất giao dịch mua của bạn! + TÔI HIỂU! + + Bạn cần có Ví AppCoins! + Để nhận phần thưởng của mình, bạn cần có Ví AppCoins. + + Thanh toán như khách + Thẻ tín dụng + PayPal + THANH TOÁN BẰNG PAYPAL + THANH TOÁN BẰNG THẺ TÍN DỤNG + Số thẻ + MM/YY + MÃ CVV + MÃ CVC/CVV + THAY ĐỔI THẺ + Thanh toán bằng Ví AppCoins + Nhận lên đến %s%% tiền thưởng! + Bạn sẽ nhận được %s trên giao dịch mua này. + Bạn sẽ nhận được một phần tiền thưởng trên giao dịch mua này. + ƯU ĐÃI TỐT NHẤT + HOÀN TẤT! + Lần tới, bạn sẽ nhận được lên đến %s%% tiền thưởng bằng Ví AppCoins! + Lần tới, bạn sẽ nhận được phần tiền thưởng bằng Ví AppCoins! + Bạn đã nhận được %s trên giao dịch mua này. + Giao dịch mua đã hoàn tất! + PHƯƠNG THỨC THANH TOÁN KHÁC + Cần trợ giúp? + Liên hệ bộ phận hỗ trợ. + + KẾ TIẾP + HỦY BỎ + MUA + CÀI ĐẶT VÍ + OK + Cài đặt + + Lỗi + Bạn đã sở hữu vật phẩm này rồi! + Rất tiếc, đã xảy ra lỗi. + Đã xảy ra sự cố với thẻ của bạn. Vui lòng thử lại hoặc liên hệ ngay với chúng tôi. + Giao dịch đã bị ngân hàng của bạn từ chối. Vui lòng thử với một thẻ khác hoặc liên hệ ngay với chúng tôi. + Có vẻ như bạn không còn đủ tiền hoặc đã đạt đến hạn mức thẻ của mình. Vui lòng thử lại bằng một thẻ khác. + Có vẻ như thẻ của bạn đã hết hạn. Hãy thử lại bằng một thẻ khác. + Bạn có chắc rằng số thẻ của mình đúng không? Vui lòng kiểm tra và thử lại lần nữa. + Loại thẻ của bạn chưa được hỗ trợ. Hãy thử lại bằng một thẻ khác. + Bạn có chắc chắn thông tin bảo mật đúng không? Vui lòng thử lại lần nữa. + Mã CVV/CVC của bạn có vẻ như không đúng. Vui lòng thử lại lần nữa. + CVV/CVC sai + Nếu vấn đề vẫn không được khắc phục, vui lòng liên hệ ngay với chúng tôi. + diff --git a/app/src/main/res/external_strings/values-vi/perks.po b/app/src/main/res/external_strings/values-vi/perks.po new file mode 100644 index 00000000000..049b0fd3844 --- /dev/null +++ b/app/src/main/res/external_strings/values-vi/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: vi\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Vietnamese\n" +"Language: vi_VN\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "Kiếm AppCoins Credits!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "Mua hàng trong {0} và nhận {1} AppCoins Credits." + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "Bởi vì bạn đã mua hàng trong{0}." + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "Dùng {0} {1} trong {2} ngày và nhận {3} AppCoins Credits." + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "Bởi vì bạn đã dùng hơn {0} {1} trong {2} ngày qua." + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "Đạt cấp kế tiếp và nhận {0} AppCoins Credits!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "Bởi vì bạn đã đạt đến cấp kế tiếp." + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "Nhận thêm {0}% tiền thưởng cho tất cả các giao dịch mua của bạn, ngoại trừ AppCoins Credits, trong {1}." + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "Nhận thêm {0}% tiền thưởng cho giao dịch mua đầu tiên của bạn, ngoại trừ AppCoins Credits, trong {1}" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "Vì giao dịch mua hàng đầu tiên của bạn trong {0}." + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "Nhận thêm {0}% tiền thưởng cho giao dịch mua đầu tiên trong ngày, ngoại trừ AppCoins Credits, trong {1}" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "Vì giao dịch mua hàng đầu tiên trong ngày của bạn trong {0}." + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} AppCoins Credits tiền thưởng!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "Vì bạn đã dùng {0} {1} trong {2}." + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values-zh-rCN/external_strings.xml b/app/src/main/res/external_strings/values-zh-rCN/external_strings.xml new file mode 100644 index 00000000000..bdc1badb45f --- /dev/null +++ b/app/src/main/res/external_strings/values-zh-rCN/external_strings.xml @@ -0,0 +1,58 @@ + + + + 要购买此物品,你需要先获取%s。 + AppCoins Wallet + 关闭 + + 你需要使用AppCoins Wallet来进行此次购买。请从Aptoide或Play Store下载,然后回来完成购买! + 明白了! + + 你需要AppCoins钱包! + 要获取奖励,你需要AppCoins钱包。 + + 以访客身份付款 + 信用卡 + PayPal + 使用PAYPAL付款 + 使用信用卡付款 + 卡号 + MM/YY + CVV + CVC/CVV + 更换卡 + 使用AppCoins Wallet付款 + 获取高达%s%%的奖励! + 你通过此次购买将获得%s。 + 你将通过此次购买获得奖励。 + 最佳优惠 + 完成! + 下次使用AppCoins Wallet付款,获取高达%s%%的奖励! + 下次使用AppCoins Wallet付款,可以获得奖励! + 你本可以通过此次购买获得%s。 + 购买完成! + 更多付款方式 + 需要帮助? + 与支持人员联系。 + + 下一步 + 取消 + 购买 + 安装钱包 + 确定 + 安装 + + 错误 + 您已拥有此物品! + 糟糕,出现错误。 + 你的卡出现问题。请重试或与我们联系。 + 交易已被你的银行拒绝。请尝试使用其他卡或与我们联系。 + 你的卡似乎资金不足或存在限制。请尝试使用其他卡。 + 你的卡似乎已经过期。请尝试使用其他卡。 + 你确定卡号正确吗?请检查,然后重试。 + 尚不支持你的卡类型。请尝试使用其他卡。 + 你确定安全信息正确吗?请重试。 + 你的CVV/CVC代码似乎有误。请重试。 + CVV/CVC错误 + 如果问题仍然存在,请与我们联系。 + diff --git a/app/src/main/res/external_strings/values-zh-rCN/perks.po b/app/src/main/res/external_strings/values-zh-rCN/perks.po new file mode 100644 index 00000000000..bc83809703a --- /dev/null +++ b/app/src/main/res/external_strings/values-zh-rCN/perks.po @@ -0,0 +1,104 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: appcoins-bds-wallet\n" +"X-Crowdin-Project-ID: 314965\n" +"X-Crowdin-Language: zh-CN\n" +"X-Crowdin-File: /translations_update/app/src/main/res/external_strings/values/perks.po\n" +"X-Crowdin-File-ID: 3840\n" +"Project-Id-Version: appcoins-bds-wallet\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Language-Team: Chinese Simplified\n" +"Language: zh_CN\n" +"PO-Revision-Date: 2020-10-15 14:26\n" + +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "赚取AppCoins积金!" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "在{0}消费,获取{1} AppCoins积金。" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "因为你在{0}消费了。" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "在{2}天内消费{0} {1},获取{3} AppCoins积金。" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "因为你在过去{2}天消费了{0}以上的{1}。" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "达到下一关可获取{0} AppCoins积金!" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "因为你已达到下一级。" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "+{0} AppCoins积金奖励!" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/external_strings/values/external_strings.xml b/app/src/main/res/external_strings/values/external_strings.xml new file mode 100644 index 00000000000..351794674f5 --- /dev/null +++ b/app/src/main/res/external_strings/values/external_strings.xml @@ -0,0 +1,66 @@ + + + +To buy this item you first need to get the %s. +AppCoins Wallet +CLOSE + + + +You need the AppCoins Wallet to make this purchase. Download it from Aptoide or Play Store and come back to complete your purchase! + GOT IT! + + +You need the AppCoins Wallet! +To get your reward you need the AppCoins Wallet. + + +Pay as a guest +Credit Card +PayPal +PAY USING PAYPAL +PAY USING CREDIT CARD +Card number +MM/YY +CVV +CVC/CVV +CHANGE CARD +Pay with AppCoins Wallet +Get up to %s%% Bonus! +You\'ll receive %s on this purchase. +You\'ll receive a Bonus on this purchase. +BEST DEAL +DONE! +Next time, get up to %s%% Bonus with the AppCoins Wallet! +Next time, get a Bonus with the AppCoins Wallet! +You could have received %s on this purchase. +Purchase completed! +MORE PAYMENT METHODS +Need help? +Contact Support. + + +NEXT +CANCEL +BUY +INSTALL WALLET +OK +Install + + + Error + You already own this item! + Oops, something went wrong. + There was a problem with your card. Please try again or contact us. + The transaction has been rejected by your bank. Please try with a different card or contact us. + It seems you don\'t have enough funds or there\'s a limit on your card. Please try with a different one. + It seems your card has expired. Please try with a different one. + Are you sure your card number is correct? Please check and try again. + Your card type is not supported yet. Try with a different one. + Are you sure the security information is correct? Please try again. + Your CVV/CVC code seems to be wrong. Please try again. + Wrong CVV/CVC + If the problem persists please contact us. + + + diff --git a/app/src/main/res/external_strings/values/perks.po b/app/src/main/res/external_strings/values/perks.po new file mode 100644 index 00000000000..3f197576790 --- /dev/null +++ b/app/src/main/res/external_strings/values/perks.po @@ -0,0 +1,91 @@ +#. Card Title +#: +msgid "Earn AppCoins Credits!" +msgstr "" + +#. Card - Purchase Promotion. +#: +msgid "Make a purchase in {0} and receive {1} AppCoins Credits." +msgstr "" + +#. Receipt - Purchase Promotion. +#: +msgid "Because you made a purchase in {0}." +msgstr "" + +#. Card - Expenditure per days Promotion. +#: +msgid "Spend {0} {1} in {2} days and receive {3} AppCoins Credits." +msgstr "" + +#. Receipt - Expenditure per days Promotion. +#: +msgid "Because you spent more than {0} {1} in the last {2} days." +msgstr "" + +#. Card - Next Level Promotion. +#: +msgid "Reach the next level and receive {0} AppCoins Credits!" +msgstr "" + +#. Receipt - Expenditure bonus. +#: +msgid "Because you reached the next level." +msgstr "" + +#. Card - Extra Gamification Bonus. +#: +msgid "Receive an extra {0}% Bonus in all your purchases, except AppCoins Credits, in {1}." +msgstr "" + +#. Card - First purchase. +#: +msgid "Receive an extra {0}% Bonus in your first purchase, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase. +#: +msgid "Because of your first purchase in {0}." +msgstr "" + +#. Card - First purchase of the day. +#: +msgid "Receive an extra {0}% Bonus in your first purchase of the day, except AppCoins Credits, in {1}" +msgstr "" + +#. Receipt - First Purchase of the day. +#: +msgid "Because of your first purchase of the day in {0}." +msgstr "" + +#. Title for bonus receipts. +#: +msgid "+{0} AppCoins Credits Bonus!" +msgstr "" + +#. Card - Expendidute per amount Promotion. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in {3}." +msgstr "" + +#. Receipt - Expenditure per amount Promotion. +#: +msgid "Because you spent {0} {1} in {2}." +msgstr "" + +#. Card - Expendidure in any game. +#: +msgid "Receive {0} AppCoins Credits extra when you spend {1} {2} in AppCoins apps." +msgstr "" + +#. Receipt - Expenditure in any game. +#: +msgid "Because you spent {0} {1} in AppCoins apps." +msgstr "" + + +#. Card - Extra bonus in all games. +#: +msgid "Receive an extra {0}% in all your purchases, except AppCoins Credits." +msgstr "" + diff --git a/app/src/main/res/font/roboto_medium.ttf b/app/src/main/res/font/roboto_medium.ttf new file mode 100755 index 00000000000..f714a514d94 Binary files /dev/null and b/app/src/main/res/font/roboto_medium.ttf differ diff --git a/app/src/main/res/layout-land/activity_my_address.xml b/app/src/main/res/layout-land/activity_my_address.xml index 92530e069d2..5744d7532eb 100644 --- a/app/src/main/res/layout-land/activity_my_address.xml +++ b/app/src/main/res/layout-land/activity_my_address.xml @@ -1,12 +1,9 @@ - -