diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 91e5999a3..a55e7a179 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,5 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index d63344830..000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,245 +0,0 @@ - - - - - - - - - - - - - - - - - - - - Android - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index cd5da0eba..c21d945bc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,14 +39,14 @@ android { main.java.srcDirs += 'src/main/kotlin' } - compileSdk 31 + compileSdk 32 defaultConfig { applicationId "com.hover.stax" minSdk 21 - targetSdk 31 - versionCode 157 - versionName "1.11.17" + targetSdk 32 + versionCode 174 + versionName "1.12.16" vectorDrawables.useSupportLibrary = true multiDexEnabled true @@ -66,7 +66,6 @@ android { kotlinOptions { jvmTarget = "1.8" - useIR = true } buildTypes { @@ -125,19 +124,18 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.4.2' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.vectordrawable:vectordrawable:1.1.0' - implementation 'androidx.navigation:navigation-fragment-ktx:2.4.2' - implementation 'androidx.navigation:navigation-ui-ktx:2.4.2' + implementation 'androidx.navigation:navigation-fragment-ktx:2.5.1' + implementation 'androidx.navigation:navigation-ui-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation "androidx.biometric:biometric:1.1.0" implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.viewpager:viewpager:1.0.0' implementation 'androidx.recyclerview:recyclerview-selection:1.1.0' implementation 'androidx.work:work-runtime-ktx:2.7.1' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' - implementation 'androidx.activity:activity-compose:1.4.0' - kapt "androidx.lifecycle:lifecycle-common-java8:2.4.1" - - implementation 'androidx.core:core-splashscreen:1.0.0-rc01' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' + implementation 'androidx.activity:activity-compose:1.5.1' + kapt "androidx.lifecycle:lifecycle-common-java8:2.5.1" + implementation 'androidx.core:core-splashscreen:1.0.0' implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' @@ -146,12 +144,19 @@ dependencies { implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation "androidx.compose.material:material-icons-extended:$compose_version" + implementation "androidx.compose.ui:ui-viewbinding:$compose_version" + implementation 'androidx.compose.runtime:runtime-livedata:1.3.0-alpha02' + implementation 'com.google.accompanist:accompanist-drawablepainter:0.25.1' //logging implementation 'com.jakewharton.timber:timber:5.0.1' + implementation 'com.uxcam:uxcam:3.4.2@aar' + implementation 'com.amplitude:android-sdk:3.35.1' + implementation 'com.appsflyer:af-android-sdk:6.8.0' + implementation 'com.android.installreferrer:installreferrer:2.2' // Firebase - implementation platform('com.google.firebase:firebase-bom:28.4.0') + implementation platform('com.google.firebase:firebase-bom:30.3.2') implementation 'com.google.firebase:firebase-crashlytics' implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-messaging' @@ -161,33 +166,31 @@ dependencies { implementation 'com.google.android.play:core:1.10.3' implementation 'com.google.firebase:firebase-perf' implementation 'com.google.firebase:firebase-firestore-ktx' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4' //auth implementation 'com.google.android.gms:play-services-auth:20.2.0' implementation 'com.google.firebase:firebase-auth-ktx' - implementation 'com.amplitude:android-sdk:3.35.1' implementation "com.squareup.okhttp3:okhttp:4.9.3" - implementation "com.googlecode.libphonenumber:libphonenumber:8.12.50" + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.53' implementation "com.github.YarikSOffice:lingver:1.3.0" - implementation 'com.appsflyer:af-android-sdk:6.6.1' - implementation 'com.android.installreferrer:installreferrer:2.2' implementation 'com.github.bumptech.glide:glide:4.13.2' kapt 'com.github.bumptech.glide:compiler:4.13.2' + implementation "io.coil-kt:coil-compose:2.1.0" - def roomVersion = "2.4.2" - implementation "androidx.room:room-ktx:$roomVersion" - implementation "androidx.room:room-runtime:$roomVersion" - kapt "androidx.room:room-compiler:$roomVersion" - androidTestImplementation "androidx.room:room-testing:$roomVersion" + implementation "androidx.room:room-ktx:$room_version" + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" + androidTestImplementation "androidx.room:room-testing:$room_version" implementation "androidx.core:core-ktx:1.8.0" - implementation "io.grpc:grpc-okhttp:1.44.1" + implementation 'io.grpc:grpc-okhttp:1.48.1' //di - def koin_version= "3.1.5" implementation "io.insert-koin:koin-android:$koin_version" + implementation "io.insert-koin:koin-androidx-compose:$koin_version" // Tests testImplementation 'junit:junit:4.13.2' @@ -195,14 +198,9 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' // Hover SDK - debugImplementation project(":hover.sdk") - def sdk_version = "2.0.0-stax-1.11.0-pro" - - // UXCam - implementation 'com.uxcam:uxcam:3.3.7@aar' - + def sdk_version = "2.0.0-stax-1.12.14-pro" releaseImplementation "com.hover:android-sdk:$sdk_version" - + debugImplementation project(":hover.sdk") debugImplementation 'com.android.volley:volley:1.2.1' debugImplementation 'com.google.android.gms:play-services-analytics:18.0.1' debugImplementation 'com.squareup.picasso:picasso:2.71828' @@ -211,4 +209,5 @@ dependencies { //compose androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" + } \ No newline at end of file diff --git a/app/schemas/com.hover.stax.database.AppDatabase/32.json b/app/schemas/com.hover.stax.database.AppDatabase/32.json index e9217f5f9..39868153d 100644 --- a/app/schemas/com.hover.stax.database.AppDatabase/32.json +++ b/app/schemas/com.hover.stax.database.AppDatabase/32.json @@ -2,11 +2,7 @@ "formatVersion": 1, "database": { "version": 32, -<<<<<<< HEAD - "identityHash": "7b995a5aacc2674bda5a1d44694c68b0", -======= "identityHash": "e9dc3dbe16f88faa4d9a646e8503ec05", ->>>>>>> development "entities": [ { "tableName": "channels", @@ -130,11 +126,7 @@ }, { "tableName": "stax_transactions", -<<<<<<< HEAD - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT NOT NULL, `action_id` TEXT NOT NULL, `environment` INTEGER NOT NULL DEFAULT 0, `transaction_type` TEXT NOT NULL, `channel_id` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'pending', `category` TEXT, `initiated_at` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, `description` TEXT, `amount` REAL, `fee` REAL, `confirm_code` TEXT, `recipient_id` TEXT, `balance` TEXT, `counterparty` TEXT)", -======= "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT NOT NULL, `action_id` TEXT NOT NULL, `environment` INTEGER NOT NULL DEFAULT 0, `transaction_type` TEXT NOT NULL, `channel_id` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'pending', `initiated_at` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, `description` TEXT, `amount` REAL, `fee` REAL, `confirm_code` TEXT, `recipient_id` TEXT, `category` TEXT, `counterparty` TEXT)", ->>>>>>> development "fields": [ { "fieldPath": "id", @@ -181,15 +173,6 @@ "defaultValue": "'pending'" }, { -<<<<<<< HEAD - "fieldPath": "category", - "columnName": "category", - "affinity": "TEXT", - "notNull": false - }, - { -======= ->>>>>>> development "fieldPath": "initiated_at", "columnName": "initiated_at", "affinity": "INTEGER", @@ -234,13 +217,8 @@ "notNull": false }, { -<<<<<<< HEAD - "fieldPath": "balance", - "columnName": "balance", -======= "fieldPath": "category", "columnName": "category", ->>>>>>> development "affinity": "TEXT", "notNull": false }, @@ -505,8 +483,6 @@ }, "indices": [], "foreignKeys": [] -<<<<<<< HEAD -======= }, { "tableName": "accounts", @@ -603,17 +579,12 @@ ] } ] ->>>>>>> development } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", -<<<<<<< HEAD - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7b995a5aacc2674bda5a1d44694c68b0')" -======= "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e9dc3dbe16f88faa4d9a646e8503ec05')" ->>>>>>> development ] } } \ No newline at end of file diff --git a/app/schemas/com.hover.stax.database.AppDatabase/33.json b/app/schemas/com.hover.stax.database.AppDatabase/33.json index bf02ed1a7..52805a31b 100644 --- a/app/schemas/com.hover.stax.database.AppDatabase/33.json +++ b/app/schemas/com.hover.stax.database.AppDatabase/33.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 33, - "identityHash": "4ce93c73680a412f95390c6e41575c61", + "identityHash": "5da73f0f0303be4fe1af75bd8911fa71", "entities": [ { "tableName": "channels", @@ -126,7 +126,7 @@ }, { "tableName": "stax_transactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT NOT NULL, `action_id` TEXT NOT NULL, `environment` INTEGER NOT NULL DEFAULT 0, `transaction_type` TEXT NOT NULL, `channel_id` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'pending', `initiated_at` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, `description` TEXT, `amount` REAL, `fee` REAL, `confirm_code` TEXT, `recipient_id` TEXT, `category` TEXT, `account_id` INTEGER, `counterparty` TEXT)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT NOT NULL, `action_id` TEXT NOT NULL, `environment` INTEGER NOT NULL DEFAULT 0, `transaction_type` TEXT NOT NULL, `channel_id` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'pending', `initiated_at` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, `description` TEXT, `amount` REAL, `fee` REAL, `confirm_code` TEXT, `recipient_id` TEXT, `category` TEXT, `counterparty` TEXT)", "fields": [ { "fieldPath": "id", @@ -222,12 +222,6 @@ "affinity": "TEXT", "notNull": false }, - { - "fieldPath": "accountId", - "columnName": "account_id", - "affinity": "INTEGER", - "notNull": false - }, { "fieldPath": "counterparty", "columnName": "counterparty", @@ -492,7 +486,7 @@ }, { "tableName": "accounts", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `alias` TEXT, `logo_url` TEXT NOT NULL, `account_no` TEXT, `channelId` INTEGER NOT NULL, `primary_color_hex` TEXT NOT NULL, `secondary_color_hex` TEXT NOT NULL, `isDefault` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `latestBalance` TEXT, `latestBalanceTimestamp` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(`channelId`) REFERENCES `channels`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `alias` TEXT NOT NULL, `logo_url` TEXT NOT NULL, `account_no` TEXT, `channelId` INTEGER NOT NULL, `primary_color_hex` TEXT NOT NULL, `secondary_color_hex` TEXT NOT NULL, `isDefault` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `latestBalance` TEXT, `latestBalanceTimestamp` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(`channelId`) REFERENCES `channels`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "name", @@ -504,7 +498,7 @@ "fieldPath": "alias", "columnName": "alias", "affinity": "TEXT", - "notNull": false + "notNull": true }, { "fieldPath": "logoUrl", @@ -596,7 +590,7 @@ "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, '4ce93c73680a412f95390c6e41575c61')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5da73f0f0303be4fe1af75bd8911fa71')" ] } } \ No newline at end of file diff --git a/app/src/main/assets/channels-prod.json b/app/src/main/assets/channels-prod.json index 4d7eee016..559e111cf 100644 --- a/app/src/main/assets/channels-prod.json +++ b/app/src/main/assets/channels-prod.json @@ -1212,7 +1212,7 @@ "menus_count": 2, "custom_actions_count": 249, "channel_actions_count": 5, - "transactions_count": 5138, + "transactions_count": 5146, "pending_percent": "unknown", "bounties_count": 5, "open_bounties_count": 5, @@ -4594,7 +4594,7 @@ "menus_count": 29, "custom_actions_count": 101, "channel_actions_count": 4, - "transactions_count": 5634, + "transactions_count": 5635, "pending_percent": "unknown", "bounties_count": 0, "open_bounties_count": 0, @@ -8419,7 +8419,7 @@ "menus_count": 17, "custom_actions_count": 248, "channel_actions_count": 6, - "transactions_count": 2666, + "transactions_count": 2668, "pending_percent": "unknown", "bounties_count": 6, "open_bounties_count": 3, @@ -9866,7 +9866,7 @@ "menus_count": 51, "custom_actions_count": 1007, "channel_actions_count": 18, - "transactions_count": 8898, + "transactions_count": 8900, "pending_percent": "unknown", "bounties_count": 12, "open_bounties_count": 11, @@ -13419,7 +13419,7 @@ "menus_count": 25, "custom_actions_count": 218, "channel_actions_count": 8, - "transactions_count": 474029, + "transactions_count": 479689, "pending_percent": "unknown", "bounties_count": 10, "open_bounties_count": 7, @@ -14036,7 +14036,7 @@ "menus_count": 46, "custom_actions_count": 191, "channel_actions_count": 27, - "transactions_count": 4260, + "transactions_count": 4265, "pending_percent": "unknown", "bounties_count": 22, "open_bounties_count": 5, @@ -14701,7 +14701,7 @@ "menus_count": 34, "custom_actions_count": 243, "channel_actions_count": 11, - "transactions_count": 559965, + "transactions_count": 565849, "pending_percent": "unknown", "bounties_count": 6, "open_bounties_count": 6, @@ -17817,7 +17817,7 @@ "menus_count": 38, "custom_actions_count": 6, "channel_actions_count": 21, - "transactions_count": 1399, + "transactions_count": 1404, "pending_percent": "unknown", "bounties_count": 19, "open_bounties_count": 10, @@ -28586,7 +28586,7 @@ "menus_count": 70, "custom_actions_count": 212, "channel_actions_count": 39, - "transactions_count": 531333, + "transactions_count": 532331, "pending_percent": "unknown", "bounties_count": 27, "open_bounties_count": 6, @@ -35644,7 +35644,7 @@ "menus_count": 33, "custom_actions_count": 0, "channel_actions_count": 22, - "transactions_count": 556, + "transactions_count": 557, "pending_percent": "unknown", "bounties_count": 9, "open_bounties_count": 5, @@ -39938,7 +39938,7 @@ "menus_count": 21, "custom_actions_count": 0, "channel_actions_count": 10, - "transactions_count": 687, + "transactions_count": 689, "pending_percent": "unknown", "bounties_count": 4, "open_bounties_count": 1, @@ -40124,7 +40124,7 @@ "menus_count": 7, "custom_actions_count": 405, "channel_actions_count": 1, - "transactions_count": 14697, + "transactions_count": 14713, "pending_percent": "unknown", "bounties_count": 0, "open_bounties_count": 0, @@ -48915,7 +48915,7 @@ "menus_count": 57, "custom_actions_count": 27, "channel_actions_count": 22, - "transactions_count": 5717, + "transactions_count": 5718, "pending_percent": "unknown", "bounties_count": 17, "open_bounties_count": 6, @@ -50097,7 +50097,7 @@ "menus_count": 0, "custom_actions_count": 0, "channel_actions_count": 3, - "transactions_count": 7, + "transactions_count": 11, "pending_percent": "unknown", "bounties_count": 3, "open_bounties_count": 3, @@ -53992,7 +53992,7 @@ "menus_count": 38, "custom_actions_count": 160, "channel_actions_count": 18, - "transactions_count": 26367, + "transactions_count": 26369, "pending_percent": "unknown", "bounties_count": 17, "open_bounties_count": 9, @@ -54818,7 +54818,7 @@ "menus_count": 26, "custom_actions_count": 3, "channel_actions_count": 18, - "transactions_count": 249, + "transactions_count": 251, "pending_percent": "unknown", "bounties_count": 4, "open_bounties_count": 1, @@ -59045,7 +59045,7 @@ "menus_count": 22, "custom_actions_count": 0, "channel_actions_count": 5, - "transactions_count": 600, + "transactions_count": 601, "pending_percent": "unknown", "bounties_count": 4, "open_bounties_count": 1, @@ -59538,7 +59538,7 @@ "menus_count": 35, "custom_actions_count": 13, "channel_actions_count": 12, - "transactions_count": 6795, + "transactions_count": 6798, "pending_percent": "unknown", "bounties_count": 9, "open_bounties_count": 1, @@ -60567,7 +60567,7 @@ "menus_count": 26, "custom_actions_count": 29, "channel_actions_count": 11, - "transactions_count": 4954, + "transactions_count": 4959, "pending_percent": "unknown", "bounties_count": 11, "open_bounties_count": 8, @@ -65066,7 +65066,7 @@ "menus_count": 28, "custom_actions_count": 0, "channel_actions_count": 8, - "transactions_count": 637, + "transactions_count": 638, "pending_percent": "unknown", "bounties_count": 6, "open_bounties_count": 1, @@ -67849,7 +67849,7 @@ "menus_count": 19, "custom_actions_count": 0, "channel_actions_count": 9, - "transactions_count": 11369, + "transactions_count": 11371, "pending_percent": "unknown", "bounties_count": 9, "open_bounties_count": 1, @@ -67923,7 +67923,7 @@ "menus_count": 25, "custom_actions_count": 0, "channel_actions_count": 6, - "transactions_count": 736, + "transactions_count": 738, "pending_percent": "unknown", "bounties_count": 2, "open_bounties_count": 0, diff --git a/app/src/main/assets/channels-staging.json b/app/src/main/assets/channels-staging.json index a7bd5ea6c..07d2698e2 100644 --- a/app/src/main/assets/channels-staging.json +++ b/app/src/main/assets/channels-staging.json @@ -28531,7 +28531,7 @@ "menus_count": 70, "custom_actions_count": 212, "channel_actions_count": 39, - "transactions_count": 519505, + "transactions_count": 519506, "pending_percent": "unknown", "bounties_count": 27, "open_bounties_count": 6, diff --git a/app/src/main/java/com/hover/stax/ApplicationInstance.kt b/app/src/main/java/com/hover/stax/ApplicationInstance.kt index 6b9477c4d..be1207da2 100644 --- a/app/src/main/java/com/hover/stax/ApplicationInstance.kt +++ b/app/src/main/java/com/hover/stax/ApplicationInstance.kt @@ -8,9 +8,7 @@ import com.appsflyer.AppsFlyerProperties import com.google.firebase.FirebaseApp import com.google.firebase.crashlytics.FirebaseCrashlytics import com.hover.sdk.api.Hover -import com.hover.stax.database.appModule -import com.hover.stax.database.dataModule -import com.hover.stax.database.networkModule +import com.hover.stax.di.* import com.hover.stax.utils.network.NetworkMonitor import com.yariksoffice.lingver.Lingver import org.koin.android.ext.koin.androidContext @@ -45,7 +43,7 @@ class ApplicationInstance : Application() { private fun initDI() { startKoin { androidContext(this@ApplicationInstance) - modules(listOf(appModule, dataModule, networkModule)) + modules(appModule + dataModule + networkModule + useCases + repositories) } } @@ -75,7 +73,7 @@ class ApplicationInstance : Application() { AppsFlyerLib.getInstance().apply { init(getString(R.string.appsflyer_key), conversionListener, this@ApplicationInstance) - if(AppsFlyerProperties.getInstance().getString(AppsFlyerProperties.APP_USER_ID) == null) + if (AppsFlyerProperties.getInstance().getString(AppsFlyerProperties.APP_USER_ID) == null) setCustomerUserId(Hover.getDeviceId(this@ApplicationInstance)) start(this@ApplicationInstance) diff --git a/app/src/main/java/com/hover/stax/RoutingActivity.kt b/app/src/main/java/com/hover/stax/RoutingActivity.kt index 0d45b4a15..411d2561c 100644 --- a/app/src/main/java/com/hover/stax/RoutingActivity.kt +++ b/app/src/main/java/com/hover/stax/RoutingActivity.kt @@ -23,20 +23,22 @@ import com.hover.sdk.api.Hover import com.hover.stax.addChannels.ChannelsViewModel import com.hover.stax.channels.ImportChannelsWorker import com.hover.stax.channels.UpdateChannelsWorker -import com.hover.stax.financialTips.FinancialTipsFragment import com.hover.stax.home.MainActivity import com.hover.stax.hover.PERM_ACTIVITY import com.hover.stax.inapp_banner.BannerUtils import com.hover.stax.notifications.PushNotificationTopicsInterface import com.hover.stax.onboarding.OnBoardingActivity +import com.hover.stax.presentation.financial_tips.FinancialTipsFragment import com.hover.stax.requests.REQUEST_LINK import com.hover.stax.schedules.ScheduleWorker import com.hover.stax.settings.BiometricChecker +import com.hover.stax.transfers.STAX_PREFIX import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.UIHelper import com.hover.stax.utils.Utils import com.uxcam.OnVerificationListener import com.uxcam.UXCam +import com.uxcam.datamodel.UXConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.json.JSONException @@ -46,7 +48,6 @@ import timber.log.Timber const val FRAGMENT_DIRECT = "fragment_direct" const val FROM_FCM = "from_notification" -const val VARIANT = "variant" class RoutingActivity : AppCompatActivity(), BiometricChecker.AuthListener, PushNotificationTopicsInterface { @@ -129,17 +130,26 @@ class RoutingActivity : AppCompatActivity(), BiometricChecker.AuthListener, Push setConfigSettingsAsync(configSettings) setDefaultsAsync(R.xml.remote_config_default) fetchAndActivate().addOnCompleteListener { - val variant = remoteConfig.getString("onboarding_variant") - Utils.saveString(VARIANT, variant, this@RoutingActivity) - + fetchConfigs(remoteConfig) + validateUser() + }.addOnFailureListener { validateUser() } } } + private fun fetchConfigs(remoteConfig: FirebaseRemoteConfig) { + val staxPrefix = remoteConfig.getString(STAX_PREFIX) + Utils.saveString(STAX_PREFIX, staxPrefix, this) + } + private fun initUxCam() { if (!BuildConfig.DEBUG) { - UXCam.startWithKey(getString(R.string.uxcam_key)) + val config = UXConfig.Builder(getString(R.string.uxcam_key)) + .enableAutomaticScreenNameTagging(false) + .enableImprovedScreenCapture(true) + .build() + UXCam.startWithConfiguration(config) UXCam.addVerificationListener(object : OnVerificationListener { override fun onVerificationSuccess() { @@ -164,7 +174,7 @@ class RoutingActivity : AppCompatActivity(), BiometricChecker.AuthListener, Push } - private fun registerUXCamPushNotification(){ + private fun registerUXCamPushNotification() { FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task -> if (!task.isSuccessful) { return@OnCompleteListener @@ -260,7 +270,7 @@ class RoutingActivity : AppCompatActivity(), BiometricChecker.AuthListener, Push finish() } - override fun onAuthError(error: String) = runOnUiThread { UIHelper.flashMessage(this, getString(R.string.toast_error_auth)) } + override fun onAuthError(error: String) = runOnUiThread { UIHelper.flashAndReportMessage(this, getString(R.string.toast_error_auth)) } override fun onAuthSuccess(action: HoverAction?) = chooseNavigation(intent) diff --git a/app/src/main/java/com/hover/stax/accounts/AccountDetailFragment.kt b/app/src/main/java/com/hover/stax/accounts/AccountDetailFragment.kt index 57def3f3f..a4e8446eb 100644 --- a/app/src/main/java/com/hover/stax/accounts/AccountDetailFragment.kt +++ b/app/src/main/java/com/hover/stax/accounts/AccountDetailFragment.kt @@ -15,9 +15,10 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.hover.sdk.actions.HoverAction import com.hover.stax.R -import com.hover.stax.balances.BalanceAdapter -import com.hover.stax.balances.BalancesViewModel +import com.hover.stax.presentation.home.BalancesViewModel import com.hover.stax.databinding.FragmentAccountBinding +import com.hover.stax.domain.model.Account +import com.hover.stax.domain.model.PLACEHOLDER import com.hover.stax.futureTransactions.FutureViewModel import com.hover.stax.futureTransactions.RequestsAdapter import com.hover.stax.futureTransactions.ScheduledAdapter @@ -34,7 +35,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel class AccountDetailFragment : Fragment(), TransactionHistoryAdapter.SelectListener, ScheduledAdapter.SelectListener, - RequestsAdapter.SelectListener, BalanceAdapter.BalanceListener { + RequestsAdapter.SelectListener { private val viewModel: AccountDetailViewModel by sharedViewModel() private val balancesViewModel: BalancesViewModel by sharedViewModel() @@ -72,7 +73,7 @@ class AccountDetailFragment : Fragment(), TransactionHistoryAdapter.SelectListen binding.balanceCard.root.cardElevation = 0F binding.balanceCard.balanceChannelName.setTextColor(ContextCompat.getColor(requireActivity(), R.color.offWhite)) binding.balanceCard.balanceAmount.setTextColor(ContextCompat.getColor(requireActivity(), R.color.offWhite)) - binding.balanceCard.balanceRefreshIcon.setOnClickListener { onTapRefresh(viewModel.account.value) } + binding.balanceCard.balanceRefreshIcon.setOnClickListener { onTapBalanceRefresh(viewModel.account.value) } } private fun setUpManage() { @@ -144,7 +145,7 @@ class AccountDetailFragment : Fragment(), TransactionHistoryAdapter.SelectListen } else binding.balanceCard.balanceSubtitle.text = getString(R.string.refresh_balance_desc) binding.feesDescription.text = getString(R.string.fees_label, acct.name) - binding.detailsCard.officialName.text = if(acct.name == PLACEHOLDER) acct.alias else acct.name + binding.detailsCard.officialName.text = if(acct.name.contains(PLACEHOLDER)) acct.alias else acct.name binding.manageCard.nicknameInput.setText(acct.alias, false) binding.manageCard.accountNumberInput.setText(acct.accountNo, false) @@ -177,20 +178,18 @@ class AccountDetailFragment : Fragment(), TransactionHistoryAdapter.SelectListen } private fun observeBalanceCheck() { - collectLatestLifecycleFlow(balancesViewModel.balanceAction) { + collectLifecycleFlow(balancesViewModel.balanceAction) { attemptCallHover(viewModel.account.value, it) } } - override fun onTapRefresh(account: Account?) { + private fun onTapBalanceRefresh(account: Account?) { account?.let { AnalyticsUtil.logAnalyticsEvent(getString(R.string.refresh_balance_single), requireContext()) balancesViewModel.requestBalance(account) } } - override fun onTapDetail(accountId: Int) { } - private fun attemptCallHover(account: Account?, action: HoverAction?) { action?.let { account?.let { callHover(account, action) } } } @@ -212,7 +211,7 @@ class AccountDetailFragment : Fragment(), TransactionHistoryAdapter.SelectListen private fun removeAccount(account: Account) { viewModel.removeAccount(account) NavHostFragment.findNavController(this).popBackStack() - UIHelper.flashMessage(requireActivity(), resources.getString(R.string.toast_confirm_acctremoved)) + UIHelper.flashAndReportMessage(requireActivity(), resources.getString(R.string.toast_confirm_acctremoved)) } private fun initRecyclerViews() { @@ -273,4 +272,6 @@ class AccountDetailFragment : Fragment(), TransactionHistoryAdapter.SelectListen _binding = null } + + } \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/accounts/AccountDetailViewModel.kt b/app/src/main/java/com/hover/stax/accounts/AccountDetailViewModel.kt index 8c741dc8c..cc86ede6c 100644 --- a/app/src/main/java/com/hover/stax/accounts/AccountDetailViewModel.kt +++ b/app/src/main/java/com/hover/stax/accounts/AccountDetailViewModel.kt @@ -3,9 +3,11 @@ package com.hover.stax.accounts import android.app.Application import androidx.lifecycle.* import com.hover.sdk.actions.HoverAction -import com.hover.stax.actions.ActionRepo +import com.hover.stax.data.local.actions.ActionRepo import com.hover.stax.channels.Channel -import com.hover.stax.channels.ChannelRepo +import com.hover.stax.data.local.channels.ChannelRepo +import com.hover.stax.data.local.accounts.AccountRepo +import com.hover.stax.domain.model.Account import com.hover.stax.transactions.StaxTransaction import com.hover.stax.transactions.TransactionHistory import com.hover.stax.transactions.TransactionRepo @@ -15,7 +17,8 @@ import java.util.* class AccountDetailViewModel(val application: Application, val repo: AccountRepo, private val transactionRepo: TransactionRepo, - private val channelRepo: ChannelRepo, val actionRepo: ActionRepo) : ViewModel() { + private val channelRepo: ChannelRepo, val actionRepo: ActionRepo +) : ViewModel() { private val id = MutableLiveData() var account: LiveData = MutableLiveData() @@ -45,13 +48,13 @@ class AccountDetailViewModel(val application: Application, val repo: AccountRepo private fun loadFeesThisYear(id: Int): LiveData? = transactionRepo.getFees(id, calendar.get(Calendar.YEAR)) - fun updateAccountName(newName: String) = viewModelScope.launch { + fun updateAccountName(newName: String) = viewModelScope.launch(Dispatchers.IO) { val a = account.value!! a.alias = newName repo.update(a) } - fun updateAccountNumber(newNumber: String) = viewModelScope.launch { + fun updateAccountNumber(newNumber: String) = viewModelScope.launch(Dispatchers.IO) { val a = account.value!! a.accountNo = newNumber repo.update(a) @@ -77,6 +80,7 @@ class AccountDetailViewModel(val application: Application, val repo: AccountRepo } repo.delete(account) + transactionRepo.deleteAccountTransactions(account.id) val accounts = repo.getAllAccounts() val changeDefault = account.isDefault @@ -92,5 +96,6 @@ class AccountDetailViewModel(val application: Application, val repo: AccountRepo } channelRepo.update(channelsToUpdate.toList()) + } } \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/accounts/AccountDropdown.kt b/app/src/main/java/com/hover/stax/accounts/AccountDropdown.kt index 367c4dc10..5316d12bd 100644 --- a/app/src/main/java/com/hover/stax/accounts/AccountDropdown.kt +++ b/app/src/main/java/com/hover/stax/accounts/AccountDropdown.kt @@ -12,12 +12,10 @@ import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition import com.hover.sdk.actions.HoverAction import com.hover.stax.R -import com.hover.stax.actions.ActionSelect +import com.hover.stax.domain.model.Account import com.hover.stax.utils.UIHelper import com.hover.stax.views.StaxDropdownLayout -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -import timber.log.Timber class AccountDropdown(context: Context, attributeSet: AttributeSet) : StaxDropdownLayout(context, attributeSet) { @@ -74,15 +72,16 @@ class AccountDropdown(context: Context, attributeSet: AttributeSet) : StaxDropdo } } - private fun updateChoices(accounts: MutableList) { + private fun updateChoices(accounts: List) { if (highlightedAccount == null) setDropdownValue(null) - accounts.add(Account("Add account")) val adapter = AccountDropdownAdapter(accounts, context) autoCompleteTextView.apply { setAdapter(adapter) setOnItemClickListener { parent, _, position, _ -> onSelect(parent.getItemAtPosition(position) as Account) } } - onSelect(accounts.firstOrNull { it.isDefault }) + + if (accounts.firstOrNull()?.id != 0) + onSelect(accounts.firstOrNull { it.isDefault }) } @@ -100,8 +99,8 @@ class AccountDropdown(context: Context, attributeSet: AttributeSet) : StaxDropdo with(viewModel) { lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - accounts.collect { - accountUpdate(it) + accountList.collect { + accountUpdate(it.accounts.plus(Account("Add account"))) } } } @@ -130,7 +129,7 @@ class AccountDropdown(context: Context, attributeSet: AttributeSet) : StaxDropdo } else if (actions.isNotEmpty() && actions.size == 1) addInfoMessage(actions.first()) else if (viewModel.activeAccount.value != null && showSelected) - setState(helperText, SUCCESS) + setState(null, SUCCESS) } private fun addInfoMessage(action: HoverAction) { diff --git a/app/src/main/java/com/hover/stax/accounts/AccountDropdownAdapter.kt b/app/src/main/java/com/hover/stax/accounts/AccountDropdownAdapter.kt index fd1ad54b7..b8f596750 100644 --- a/app/src/main/java/com/hover/stax/accounts/AccountDropdownAdapter.kt +++ b/app/src/main/java/com/hover/stax/accounts/AccountDropdownAdapter.kt @@ -7,7 +7,7 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import com.hover.stax.R import com.hover.stax.databinding.StaxSpinnerItemWithLogoBinding -import com.hover.stax.utils.UIHelper +import com.hover.stax.domain.model.Account import com.hover.stax.utils.UIHelper.loadImage class AccountDropdownAdapter(val accounts: List, context: Context) : ArrayAdapter(context, 0, accounts) { @@ -41,7 +41,7 @@ class AccountDropdownAdapter(val accounts: List, context: Context) : Ar fun setAccount(account: Account) { binding.serviceItemNameId.text = account.alias - if(account.logoUrl.isEmpty()) + if (account.logoUrl.isEmpty()) binding.serviceItemImageId.loadImage(binding.root.context, R.drawable.ic_add) else binding.serviceItemImageId.loadImage(binding.root.context, account.logoUrl) diff --git a/app/src/main/java/com/hover/stax/accounts/AccountsAdapter.kt b/app/src/main/java/com/hover/stax/accounts/AccountsAdapter.kt index 515c20188..4476d10ec 100644 --- a/app/src/main/java/com/hover/stax/accounts/AccountsAdapter.kt +++ b/app/src/main/java/com/hover/stax/accounts/AccountsAdapter.kt @@ -5,6 +5,7 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.hover.stax.databinding.StaxSpinnerItemWithLogoBinding import com.hover.stax.R +import com.hover.stax.domain.model.Account import com.hover.stax.utils.GlideApp class AccountsAdapter(var accounts: List) : RecyclerView.Adapter() { diff --git a/app/src/main/java/com/hover/stax/accounts/AccountsViewModel.kt b/app/src/main/java/com/hover/stax/accounts/AccountsViewModel.kt index e7c1175d6..f4c968bc1 100644 --- a/app/src/main/java/com/hover/stax/accounts/AccountsViewModel.kt +++ b/app/src/main/java/com/hover/stax/accounts/AccountsViewModel.kt @@ -8,22 +8,25 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.hover.sdk.actions.HoverAction import com.hover.stax.R -import com.hover.stax.actions.ActionRepo -import com.hover.stax.bonus.BonusRepo +import com.hover.stax.data.local.accounts.AccountRepo +import com.hover.stax.data.local.actions.ActionRepo +import com.hover.stax.domain.model.Account +import com.hover.stax.domain.model.PLACEHOLDER import com.hover.stax.schedules.Schedule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class AccountsViewModel(application: Application, val repo: AccountRepo, val actionRepo: ActionRepo, private val bonusRepo: BonusRepo) : AndroidViewModel(application), +class AccountsViewModel(application: Application, val repo: AccountRepo, val actionRepo: ActionRepo) : AndroidViewModel(application), AccountDropdown.HighlightListener { - private val _accounts = MutableStateFlow>(emptyList()) - val accounts: StateFlow> = _accounts + private val _accounts = MutableStateFlow(AccountList()) + val accountList = _accounts.asStateFlow() + val activeAccount = MutableLiveData() private var type = MutableLiveData() @@ -42,10 +45,10 @@ class AccountsViewModel(application: Application, val repo: AccountRepo, val act } private fun fetchAccounts() = viewModelScope.launch { - repo.getAccounts().collect { - _accounts.value = it + repo.getAccounts().collect { a -> + _accounts.update { it.copy(accounts = a) } - setActiveAccountIfNull(it) + setActiveAccountIfNull(a) } } @@ -63,17 +66,15 @@ class AccountsViewModel(application: Application, val repo: AccountRepo, val act private fun loadActions(type: String?) { if (type == null || activeAccount.value == null) return - if (accounts.value.isEmpty()) return + if (accountList.value.accounts.isEmpty()) return + loadActions(activeAccount.value!!, type) } private fun loadActions(account: Account?) { if (account == null || type.value.isNullOrEmpty()) return - if (type.value == HoverAction.AIRTIME) - checkForBonus(account) - else - loadActions(account, type.value!!) + loadActions(account, type.value!!) } private fun loadActions(account: Account, t: String) = viewModelScope.launch(Dispatchers.IO) { @@ -83,21 +84,7 @@ class AccountsViewModel(application: Application, val repo: AccountRepo, val act ) } - private fun loadActions(channelId: Int, t: String = HoverAction.AIRTIME) = viewModelScope.launch(Dispatchers.IO) { - val actions = actionRepo.getActions(channelId, t) - channelActions.postValue(actions) - } - - private fun checkForBonus(account: Account) = viewModelScope.launch(Dispatchers.IO) { - val bonus = bonusRepo.getBonusByUserChannel(account.channelId) - - if (bonus != null) - loadActions(bonus.purchaseChannel) - else - loadActions(account, type.value!!) - } - - fun setActiveAccount(accountId: Int?) = accountId?.let { activeAccount.postValue(accounts.value.find { it.id == accountId }) } + fun setActiveAccount(accountId: Int?) = accountId?.let { activeAccount.postValue(accountList.value.accounts.find { it.id == accountId }) } fun setActiveAccountFromChannel(userChannelId: Int) = viewModelScope.launch { repo.getAccounts().collect { accounts -> @@ -117,19 +104,19 @@ class AccountsViewModel(application: Application, val repo: AccountRepo, val act } } - fun isValidAccount(): Boolean = activeAccount.value!!.name != PLACEHOLDER + fun isValidAccount(): Boolean = !activeAccount.value!!.name.contains(PLACEHOLDER) fun view(s: Schedule) { setType(s.type) } fun reset() { - activeAccount.value = accounts.value.firstOrNull { it.isDefault } + activeAccount.value = accountList.value.accounts.firstOrNull { it.isDefault } } fun setDefaultAccount(account: Account) = viewModelScope.launch(Dispatchers.IO) { - if (accounts.value.isNotEmpty()) { - val accts = accounts.value + if (accountList.value.accounts.isNotEmpty()) { + val accts = accountList.value.accounts //remove current default account val current: Account? = accts.firstOrNull { it.isDefault } @@ -150,4 +137,6 @@ class AccountsViewModel(application: Application, val repo: AccountRepo, val act activeAccount.postValue(account) } -} \ No newline at end of file +} + +data class AccountList(val accounts: List = emptyList()) \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/accounts/ChannelWithAccounts.kt b/app/src/main/java/com/hover/stax/accounts/ChannelWithAccounts.kt index da85c4758..fe40325e3 100644 --- a/app/src/main/java/com/hover/stax/accounts/ChannelWithAccounts.kt +++ b/app/src/main/java/com/hover/stax/accounts/ChannelWithAccounts.kt @@ -3,6 +3,7 @@ package com.hover.stax.accounts import androidx.room.Embedded import androidx.room.Relation import com.hover.stax.channels.Channel +import com.hover.stax.domain.model.Account data class ChannelWithAccounts( @Embedded diff --git a/app/src/main/java/com/hover/stax/actions/ActionSelectViewModel.kt b/app/src/main/java/com/hover/stax/actions/ActionSelectViewModel.kt index 03779542c..de84e9fdf 100644 --- a/app/src/main/java/com/hover/stax/actions/ActionSelectViewModel.kt +++ b/app/src/main/java/com/hover/stax/actions/ActionSelectViewModel.kt @@ -5,10 +5,11 @@ import android.content.Context import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData -import com.hover.stax.accounts.ACCOUNT_NAME + import com.hover.sdk.actions.HoverAction import com.hover.sdk.actions.HoverAction.* import com.hover.stax.R +import com.hover.stax.domain.model.ACCOUNT_NAME import java.util.LinkedHashMap const val RECIPIENT_INSTITUTION = "recipientInstitution" diff --git a/app/src/main/java/com/hover/stax/addChannels/AddChannelsFragment.kt b/app/src/main/java/com/hover/stax/addChannels/AddChannelsFragment.kt index 961a61a68..d51150b17 100644 --- a/app/src/main/java/com/hover/stax/addChannels/AddChannelsFragment.kt +++ b/app/src/main/java/com/hover/stax/addChannels/AddChannelsFragment.kt @@ -22,7 +22,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.WorkManager import com.hover.sdk.sims.SimInfo import com.hover.stax.R -import com.hover.stax.accounts.Account +import com.hover.stax.domain.model.Account import com.hover.stax.accounts.AccountsAdapter import com.hover.stax.bonus.BonusViewModel import com.hover.stax.channels.Channel @@ -120,10 +120,6 @@ class AddChannelsFragment : Fragment(), ChannelsAdapter.SelectListener, CountryA } private fun fillUpChannelLists() { - binding.selectedList.apply { - layoutManager = UIHelper.setMainLinearManagers(requireContext()) - } - binding.channelsList.apply { layoutManager = UIHelper.setMainLinearManagers(requireContext()) adapter = selectAdapter @@ -175,12 +171,12 @@ class AddChannelsFragment : Fragment(), ChannelsAdapter.SelectListener, CountryA binding.channelsListCard.hideProgressIndicator() showSelected(accounts.isNotEmpty()) - if (accounts.isNotEmpty()) - binding.selectedList.adapter = AccountsAdapter(accounts) +// if (accounts.isNotEmpty()) +// binding.selectedList.adapter = AccountsAdapter(accounts) } private fun showSelected(visible: Boolean) { - binding.selectedChannelsCard.visibility = if (visible) VISIBLE else GONE +// binding.selectedChannelsCard.visibility = if (visible) VISIBLE else GONE binding.channelsListCard.setBackButtonVisibility(if (visible) GONE else VISIBLE) } diff --git a/app/src/main/java/com/hover/stax/addChannels/ChannelsViewModel.kt b/app/src/main/java/com/hover/stax/addChannels/ChannelsViewModel.kt index 124b99e79..8284a4ff5 100644 --- a/app/src/main/java/com/hover/stax/addChannels/ChannelsViewModel.kt +++ b/app/src/main/java/com/hover/stax/addChannels/ChannelsViewModel.kt @@ -13,14 +13,15 @@ import com.hover.sdk.api.ActionApi import com.hover.sdk.api.Hover import com.hover.sdk.sims.SimInfo import com.hover.stax.R -import com.hover.stax.accounts.Account -import com.hover.stax.accounts.AccountRepo -import com.hover.stax.accounts.PLACEHOLDER -import com.hover.stax.actions.ActionRepo -import com.hover.stax.bonus.BonusRepo +import com.hover.stax.domain.model.Account +import com.hover.stax.data.local.accounts.AccountRepo +import com.hover.stax.data.local.actions.ActionRepo +import com.hover.stax.data.local.bonus.BonusRepo import com.hover.stax.channels.Channel -import com.hover.stax.channels.ChannelRepo +import com.hover.stax.channels.ChannelUtil.updateChannels +import com.hover.stax.data.local.channels.ChannelRepo import com.hover.stax.countries.CountryAdapter +import com.hover.stax.domain.model.PLACEHOLDER import com.hover.stax.notifications.PushNotificationTopicsInterface import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.Utils @@ -28,7 +29,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel as KChannel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import org.json.JSONObject @@ -182,7 +182,7 @@ class ChannelsViewModel(application: Application, val repo: ChannelRepo, val acc val defaultAccount = accountRepo.getDefaultAccount() val accounts = channels.mapIndexed { index, channel -> - val accountName: String = if (getFetchAccountAction(channel.id) == null) channel.name else PLACEHOLDER //placeholder alias for easier identification later + val accountName: String = if (getFetchAccountAction(channel.id) == null) channel.name else channel.name.plus(PLACEHOLDER) //ensures uniqueness of name due to db constraints Account( accountName, channel.name, channel.logoUrl, channel.accountNo, channel.id, channel.countryAlpha2, channel.id, channel.primaryColorHex, channel.secondaryColorHex, defaultAccount == null && index == 0 @@ -192,8 +192,8 @@ class ChannelsViewModel(application: Application, val repo: ChannelRepo, val acc ActionApi.scheduleActionConfigUpdate(it.countryAlpha2, 24, getApplication()) } - channels.onEach { it.selected = true }.also { repo.update(it) } val accountIds = accountRepo.insert(accounts) + channels.onEach { it.selected = true }.also { repo.update(it) } promptBalanceCheck(accountIds.first().toInt()) } diff --git a/app/src/main/java/com/hover/stax/balances/BalanceAdapter.kt b/app/src/main/java/com/hover/stax/balances/BalanceAdapter.kt deleted file mode 100644 index b8376c00b..000000000 --- a/app/src/main/java/com/hover/stax/balances/BalanceAdapter.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.hover.stax.balances - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.DrawableCompat -import androidx.recyclerview.widget.RecyclerView -import com.hover.stax.R -import com.hover.stax.accounts.Account -import com.hover.stax.accounts.DUMMY -import com.hover.stax.databinding.BalanceItemBinding -import com.hover.stax.utils.DateUtils -import com.hover.stax.utils.UIHelper -import com.hover.stax.utils.Utils -import timber.log.Timber - - -class BalanceAdapter(val accounts: List, val balanceListener: BalanceListener?) : RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BalancesViewHolder { - val binding = BalanceItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return BalancesViewHolder(binding) - } - - override fun onBindViewHolder(holder: BalancesViewHolder, position: Int) { - val account = accounts[holder.adapterPosition] - holder.bindItems(account, holder) - } - - override fun getItemCount(): Int = accounts.size - - private fun setColors(holder: BalancesViewHolder, primary: Int, secondary: Int) { - holder.binding.root.setCardBackgroundColor(primary) - holder.binding.balanceSubtitle.setTextColor(secondary) - holder.binding.balanceAmount.setTextColor(secondary) - holder.binding.balanceChannelName.setTextColor(secondary) - holder.binding.balanceRefreshIcon.setColorFilter(secondary) - } - - private fun setColorForEmptyAmount(holder: BalancesViewHolder, secondary: Int) { - var drawable = ContextCompat.getDrawable(holder.itemView.context, R.drawable.ic_remove) - - if (drawable != null) { - drawable = DrawableCompat.wrap(drawable) - DrawableCompat.setTint(drawable.mutate(), secondary) - holder.binding.balanceAmount.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null) - } - } - - inner class BalancesViewHolder(val binding: BalanceItemBinding) : RecyclerView.ViewHolder(binding.root) { - - fun bindItems(account: Account, holder: BalancesViewHolder) { - UIHelper.setTextUnderline(binding.balanceChannelName, account.alias) - - binding.balanceSubtitle.visibility = View.GONE - - when { - account.latestBalance != null -> { - binding.balanceSubtitle.visibility = View.VISIBLE - binding.balanceSubtitle.text = DateUtils.humanFriendlyDateTime(account.latestBalanceTimestamp) - binding.balanceAmount.text = Utils.formatAmount(account.latestBalance!!) - } - account.latestBalance == null -> { - binding.balanceAmount.text = "-" - binding.balanceSubtitle.visibility = View.VISIBLE - binding.balanceSubtitle.text = itemView.context.getString(R.string.refresh_balance_desc) - } - else -> { - binding.balanceAmount.text = "" - setColorForEmptyAmount(holder, UIHelper.getColor(account.secondaryColorHex, false, binding.root.context)) - } - } - - setColors( - holder, UIHelper.getColor(account.primaryColorHex, true, holder.itemView.context), - UIHelper.getColor(account.secondaryColorHex, false, holder.itemView.context) - ) - - if (account.id == DUMMY) { - holder.binding.balanceSubtitle.visibility = View.GONE - holder.binding.balanceRefreshIcon.setImageResource(R.drawable.ic_add_icon_24) - } - - binding.root.setOnClickListener { - balanceListener?.onTapDetail(account.id) - } - - binding.balanceRefreshIcon.setOnClickListener { - balanceListener?.onTapRefresh(account) - } - } - } - - interface BalanceListener { - fun onTapRefresh(account: Account?) - - fun onTapDetail(accountId: Int) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/balances/BalanceCardStackAdapter.kt b/app/src/main/java/com/hover/stax/balances/BalanceCardStackAdapter.kt deleted file mode 100644 index 6edb92d0e..000000000 --- a/app/src/main/java/com/hover/stax/balances/BalanceCardStackAdapter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.hover.stax.balances - -import android.content.Context -import android.view.ViewGroup -import com.hover.stax.accounts.Account -import com.hover.stax.databinding.StackBalanceCardBinding -import com.hover.stax.utils.UIHelper -import com.hover.stax.views.staxcardstack.StaxCardStackAdapter -import com.hover.stax.views.staxcardstack.StaxCardStackView - -class BalanceCardStackAdapter(private val ctx: Context) : StaxCardStackAdapter(ctx) { - - override fun onCreateView(parent: ViewGroup?, viewType: Int): StaxCardStackView.ViewHolder { - return MyViewHolder(StackBalanceCardBinding.inflate(layoutInflater, parent, false)) - } - - override fun bindView(account: Account, position: Int, holder: StaxCardStackView.ViewHolder?) { - if (holder is MyViewHolder) holder.bind(account.primaryColorHex) - } - - inner class MyViewHolder(val binding: StackBalanceCardBinding) : StaxCardStackView.ViewHolder(binding.root) { - - fun bind(hex: String) { - binding.root.setCardBackgroundColor(UIHelper.getColor(hex, false, ctx)) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/balances/BalancesFragment.kt b/app/src/main/java/com/hover/stax/balances/BalancesFragment.kt deleted file mode 100644 index 27b65fa20..000000000 --- a/app/src/main/java/com/hover/stax/balances/BalancesFragment.kt +++ /dev/null @@ -1,223 +0,0 @@ -package com.hover.stax.balances - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.cardview.widget.CardView -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.RecyclerView -import com.hover.sdk.actions.HoverAction -import com.hover.stax.MainNavigationDirections -import com.hover.stax.R -import com.hover.stax.accounts.Account -import com.hover.stax.accounts.AccountsViewModel -import com.hover.stax.accounts.DUMMY -import com.hover.stax.addChannels.ChannelsViewModel -import com.hover.stax.databinding.FragmentBalanceBinding -import com.hover.stax.home.HomeFragmentDirections -import com.hover.stax.home.MainActivity -import com.hover.stax.hover.AbstractHoverCallerActivity -import com.hover.stax.utils.AnalyticsUtil -import com.hover.stax.utils.UIHelper -import com.hover.stax.utils.collectLatestLifecycleFlow -import com.hover.stax.views.StaxDialog -import com.hover.stax.views.staxcardstack.StaxCardStackView -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch -import org.koin.androidx.viewmodel.ext.android.sharedViewModel - - -class BalancesFragment : Fragment(), BalanceAdapter.BalanceListener { - - private lateinit var addAccountBtn: CardView - private lateinit var balanceTitle: TextView - private lateinit var balanceStack: StaxCardStackView - private lateinit var balancesRecyclerView: RecyclerView - - private var _binding: FragmentBalanceBinding? = null - private val binding get() = _binding!! - - private val accountsViewModel: AccountsViewModel by sharedViewModel() - private val balancesViewModel: BalancesViewModel by sharedViewModel() - private val channelsViewModel: ChannelsViewModel by sharedViewModel() - private lateinit var cardStackAdapter: BalanceCardStackAdapter - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = FragmentBalanceBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setUpBalances() - setUpLinkNewAccount() - } - - private fun setUpBalances() { - setUpBalanceHeader() - setUpBalanceList() - setUpHiddenStack() - - balancesViewModel.showBalances.observe(viewLifecycleOwner) { showBalanceCards(it) } - - balancesViewModel.accounts.observe(viewLifecycleOwner) { - updateAccounts(ArrayList(it)) - } - - collectLatestLifecycleFlow(balancesViewModel.balanceAction) { - attemptCallHover(balancesViewModel.userRequestedBalanceAccount.value, it) - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - channelsViewModel.accountCallback.collect { - askToCheckBalance(it) - } - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - balancesViewModel.actionRunError.collect { - UIHelper.flashMessage(requireActivity(), it) - } - } - } - } - - private fun attemptCallHover(account: Account?, action: HoverAction?) { - action?.let { account?.let { callHover(account, action) } } - } - - private fun callHover(account: Account, action: HoverAction) { - (requireActivity() as AbstractHoverCallerActivity).runSession(account, action) - } - - private fun setUpLinkNewAccount() { - addAccountBtn = binding.newAccountLink - addAccountBtn.setOnClickListener { - (requireActivity() as MainActivity).checkPermissionsAndNavigate(MainNavigationDirections.actionGlobalAddChannelsFragment()) - } - } - - private fun setUpBalanceHeader() { - balanceTitle = binding.homeCardBalances.balanceHeaderTitleId.also { - it.setCompoundDrawablesRelativeWithIntrinsicBounds( - if (balancesViewModel.showBalances.value == true) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off, - 0, - 0, - 0 - ) - it.setOnClickListener { balancesViewModel.setBalanceState(!balancesViewModel.showBalances.value!!) } - } - } - - private fun setUpBalanceList() { - balancesRecyclerView = binding.homeCardBalances.balancesRecyclerView.apply { - layoutManager = UIHelper.setMainLinearManagers(context) - setHasFixedSize(true) - visibility = View.GONE - } - } - - private fun setUpHiddenStack() { - cardStackAdapter = BalanceCardStackAdapter(requireActivity()) - balanceStack = binding.stackBalanceCards - balanceStack.apply { - setAdapter(cardStackAdapter) - setOverlapGaps(STACK_OVERLAY_GAP) - rotationX = ROTATE_UPSIDE_DOWN - } - } - - private fun showBalanceCards(show: Boolean) { - AnalyticsUtil.logAnalyticsEvent(getString(if (balancesViewModel.showBalances.value != true) R.string.show_balances else R.string.hide_balances), requireActivity()) - balanceTitle.setCompoundDrawablesRelativeWithIntrinsicBounds( - if (show) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off, 0, 0, 0 - ) - - showAddAccount(accountsViewModel.accounts.value, show) - if (show) binding.homeCardBalances.balancesMl.transitionToEnd() else binding.homeCardBalances.balancesMl.transitionToStart() - - balanceStack.visibility = if (show) View.GONE else View.VISIBLE - } - - private fun updateAccounts(accounts: ArrayList) { - accounts.let { - addDummyAccountsIfRequired(accounts) - cardStackAdapter.updateData(accounts.reversed()) - updateBalanceCardStackHeight(accounts.size) - showAddAccount(accounts, balancesViewModel.showBalances.value!!) - } - - val balancesAdapter = BalanceAdapter(accounts, this) - balancesRecyclerView.adapter = balancesAdapter - showBalanceCards(balancesViewModel.showBalances.value!!) - } - - private fun askToCheckBalance(account: Account) { - val dialog = StaxDialog(requireActivity()) - .setDialogTitle(R.string.check_balance_title) - .setDialogMessage(R.string.check_balance_desc) - .setNegButton(R.string.later, null) - .setPosButton(R.string.check_balance_title) { onTapRefresh(account) } - dialog.showIt() - } - - private fun updateBalanceCardStackHeight(numOfItems: Int) { - val params = balanceStack.layoutParams - params.height = 20 * numOfItems - balanceStack.layoutParams = params - } - - private fun showAddAccount(accounts: List?, show: Boolean) { - addAccountBtn.visibility = if (!accounts.isNullOrEmpty() && accounts.size > 1 && show) View.VISIBLE else View.GONE - } - - private fun addDummyAccountsIfRequired(accounts: ArrayList?) { - accounts?.let { - if (it.isEmpty()) { - accounts.add(Account(getString(R.string.your_main_account), GREEN_BG).dummy()) - accounts.add(Account(getString(R.string.your_other_account), BLUE_BG).dummy()) - } - if (it.size == 1) - accounts.add(Account(getString(R.string.your_other_account), BLUE_BG).dummy()) - } - } - - override fun onTapRefresh(account: Account?) { - if (account == null || account.id == DUMMY) - (requireActivity() as MainActivity).checkPermissionsAndNavigate(HomeFragmentDirections.actionNavigationHomeToNavigationLinkAccount()) - else { - AnalyticsUtil.logAnalyticsEvent(getString(R.string.refresh_balance_single), requireContext()) - balancesViewModel.requestBalance(account) - } - } - - override fun onTapDetail(accountId: Int) { - if (accountId == DUMMY) - (requireActivity() as MainActivity).checkPermissionsAndNavigate(HomeFragmentDirections.actionNavigationHomeToNavigationLinkAccount()) - else - findNavController().navigate(HomeFragmentDirections.actionNavigationHomeToAccountDetailsFragment(accountId)) - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - companion object { - const val GREEN_BG = "#46E6CC" - const val BLUE_BG = "#04CCFC" - - const val STACK_OVERLAY_GAP = 10 - const val ROTATE_UPSIDE_DOWN = 180f - const val BALANCE_VISIBILITY_KEY: String = "BALANCE_VISIBLE" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/bonus/BonusViewModel.kt b/app/src/main/java/com/hover/stax/bonus/BonusViewModel.kt index 389197a7c..03d564743 100644 --- a/app/src/main/java/com/hover/stax/bonus/BonusViewModel.kt +++ b/app/src/main/java/com/hover/stax/bonus/BonusViewModel.kt @@ -6,12 +6,13 @@ import com.google.firebase.firestore.ktx.firestore import com.google.firebase.firestore.ktx.firestoreSettings import com.google.firebase.ktx.Firebase import com.hover.stax.channels.Channel -import com.hover.stax.channels.ChannelRepo +import com.hover.stax.data.local.channels.ChannelRepo +import com.hover.stax.data.local.bonus.BonusRepo +import com.hover.stax.domain.model.Bonus import com.hover.stax.utils.toHni import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber diff --git a/app/src/main/java/com/hover/stax/bounties/BountyAdapter.kt b/app/src/main/java/com/hover/stax/bounties/BountyAdapter.kt deleted file mode 100644 index 0686426ec..000000000 --- a/app/src/main/java/com/hover/stax/bounties/BountyAdapter.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.hover.stax.bounties - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.hover.stax.databinding.BountyCardChannelBinding - -class BountyAdapter(private val selectListener: BountyListItem.SelectListener) : ListAdapter(diffUtil) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BountyViewHolder { - return BountyViewHolder(BountyCardChannelBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - } - - override fun onBindViewHolder(holder: BountyViewHolder, position: Int) { - getItem(holder.adapterPosition)?.let { - holder.bindItems(it, selectListener) - } - } - - class BountyViewHolder(var binding: BountyCardChannelBinding) : RecyclerView.ViewHolder(binding.root) { - - fun bindItems(channelBounties: ChannelBounties, listener: BountyListItem.SelectListener) { - binding.bountyChannelCard.setTitle(channelBounties.channel.ussdName) - binding.bountyList.removeAllViews() - - for (b in channelBounties.bounties) { - val bountyLi = BountyListItem(binding.bountyChannelCard.context, null) - bountyLi.setBounty(b, listener) - binding.bountyList.addView(bountyLi) - } - } - - } - - companion object { - private val diffUtil = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ChannelBounties, newItem: ChannelBounties): Boolean { - return oldItem.channel.id == newItem.channel.id - } - - override fun areContentsTheSame(oldItem: ChannelBounties, newItem: ChannelBounties): Boolean { - return oldItem.channel == newItem.channel - } - } - } - -} diff --git a/app/src/main/java/com/hover/stax/bounties/BountyListItem.kt b/app/src/main/java/com/hover/stax/bounties/BountyListItem.kt deleted file mode 100644 index 9a0d1fb26..000000000 --- a/app/src/main/java/com/hover/stax/bounties/BountyListItem.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.hover.stax.bounties - -import android.content.Context -import android.text.Spannable -import android.text.Spanned -import android.text.method.LinkMovementMethod -import android.text.style.StrikethroughSpan -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.view.View.OnClickListener -import android.widget.LinearLayout -import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.core.text.HtmlCompat -import com.hover.stax.R -import com.hover.stax.databinding.BountyListItemBinding - -class BountyListItem(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) { - - private val binding: BountyListItemBinding - private var bounty: Bounty? = null - private var selectListener: SelectListener? = null - - fun setBounty(b: Bounty?, listener: SelectListener?) { - bounty = b - setContent() - chooseState() - selectListener = listener - } - - private fun setContent() { - binding.liTitle.text = bounty!!.generateDescription(context) - binding.liAmount.text = context.getString(R.string.bounty_amount_with_currency, bounty!!.action.bounty_amount) - } - - private fun chooseState() { - when { - bounty!!.hasASuccessfulTransaction() -> { - setState(R.color.muted_green, R.string.done, R.drawable.ic_check, false, null) - } - bounty!!.isLastTransactionFailed() && !bounty!!.action.bounty_is_open -> { - setState( - R.color.stax_bounty_red_bg, R.string.bounty_transaction_failed, R.drawable.ic_error, - false, - navTransactionDetail() - ) - } - bounty!!.isLastTransactionFailed() && bounty!!.action.bounty_is_open -> { - setState( - R.color.stax_bounty_red_bg, R.string.bounty_transaction_failed_try_again, R.drawable.ic_error, - true, - showBountyDetail() - ) - } - !bounty!!.action.bounty_is_open -> { // This bounty is closed and done by another user - setState(R.color.lighter_grey, 0, 0, false, null) - } - bounty!!.transactionCount > 0 -> { // Bounty is open and with a transaction by current user - setState( - R.color.pending_brown, R.string.bounty_pending_short_desc, R.drawable.ic_warning, - true, - navTransactionDetail() - ) - } - else -> setState(R.color.cardViewColor, 0, 0, true, showBountyDetail()) - } - } - - private fun navTransactionDetail(): OnClickListener { - return OnClickListener { - selectListener!!.viewTransactionDetail( - bounty!!.transactions[bounty!!.lastTransactionIndex()].uuid - ) - } - } - - private fun showBountyDetail(): OnClickListener { - return OnClickListener { bounty?.let { selectListener!!.viewBountyDetail(bounty!!) } } - } - - private fun setState( - color: Int, - noticeString: Int, - noticeIcon: Int, - isOpen: Boolean, - listener: OnClickListener? - ) { - setBackgroundColor(ContextCompat.getColor(context, color)) - - if (noticeString != 0) { - binding.liDetail.text = HtmlCompat.fromHtml(context.getString(noticeString), HtmlCompat.FROM_HTML_MODE_LEGACY) - binding.liDetail.movementMethod = LinkMovementMethod.getInstance() - } - - binding.liDetail.setCompoundDrawablesWithIntrinsicBounds(noticeIcon, 0, 0, 0) - binding.liDetail.visibility = if (noticeString != 0) View.VISIBLE else View.GONE - - if (!isOpen) strikeThrough(binding.liAmount) - if (!isOpen) strikeThrough(binding.liTitle) - - setOnClickListener(listener) - } - - private fun strikeThrough(textView: TextView) { - textView.setText(textView.text, TextView.BufferType.SPANNABLE) - val spannable: Spannable = textView.text as Spannable - spannable.setSpan( - StrikethroughSpan(), - 0, - textView.text.length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } - - interface SelectListener { - fun viewTransactionDetail(uuid: String?) - fun viewBountyDetail(b: Bounty) - } - - init { - binding = BountyListItemBinding.inflate(LayoutInflater.from(context), this, true) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/bounties/BountyViewModel.kt b/app/src/main/java/com/hover/stax/bounties/BountyViewModel.kt deleted file mode 100644 index 3818de388..000000000 --- a/app/src/main/java/com/hover/stax/bounties/BountyViewModel.kt +++ /dev/null @@ -1,172 +0,0 @@ -package com.hover.stax.bounties - -import android.app.Application -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import androidx.lifecycle.* -import androidx.localbroadcastmanager.content.LocalBroadcastManager -import com.hover.sdk.actions.HoverAction -import com.hover.sdk.api.Hover -import com.hover.sdk.sims.SimInfo -import com.hover.stax.actions.ActionRepo -import com.hover.stax.channels.Channel -import com.hover.stax.channels.ChannelRepo -import com.hover.stax.countries.CountryAdapter -import com.hover.stax.transactions.StaxTransaction -import com.hover.stax.transactions.TransactionRepo -import com.hover.stax.utils.Utils.getPackage -import kotlinx.coroutines.* - -private const val MAX_LOOKUP_COUNT = 40 - -class BountyViewModel(application: Application, val repo: ChannelRepo, val actionRepo: ActionRepo, transactionRepo: TransactionRepo) : AndroidViewModel(application) { - - @JvmField - var country: String = CountryAdapter.CODE_ALL_COUNTRIES - - val actions: LiveData> - val channels: LiveData> - val transactions: LiveData> - private val bountyList = MediatorLiveData>() - - private var _channelCountryList = MediatorLiveData>() - val channelCountryList: LiveData> = _channelCountryList - - var sims: MutableLiveData> = MutableLiveData() - private lateinit var bountyListAsync: Deferred> - - private var simReceiver: BroadcastReceiver? = null - - init { - simReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - viewModelScope.launch { - sims.postValue(repo.presentSims) - } - } - } - - loadSims() - actions = actionRepo.bountyActions - channels = Transformations.switchMap(actions, this::loadChannels) - _channelCountryList.addSource(channels, this::loadCountryList) - transactions = transactionRepo.bountyTransactions!! - bountyList.apply { - addSource(actions, this@BountyViewModel::makeBounties) - addSource(transactions, this@BountyViewModel::makeBountiesIfActions) - } - } - - private fun loadSims() { - viewModelScope.launch { - sims.postValue(repo.presentSims) - } - - simReceiver?.let { - LocalBroadcastManager.getInstance(getApplication()) - .registerReceiver(it, IntentFilter(getPackage(getApplication()) + ".NEW_SIM_INFO_ACTION")) - } - Hover.updateSimInfo(getApplication()) - } - - fun isSimPresent(b: Bounty): Boolean { - if (sims.value.isNullOrEmpty()) return false - for (sim in sims.value!!) { - for (i in 0 until b.action.hni_list.length()) if (b.action.hni_list.optString(i) == sim.osReportedHni) return true - } - return false - } - - private fun loadChannels(actions: List?): LiveData> { - if (actions == null) return MutableLiveData() - val ids = getChannelIdArray(actions.distinctBy { it.id }).toList() - - val channelList = runBlocking { - getChannelsAsync(ids).await() - } - - return MutableLiveData(channelList) - } - - private fun loadCountryList(channels: List) = viewModelScope.launch { - val countryCodes = mutableListOf(country) - countryCodes.addAll(channels.map { it.countryAlpha2 }.distinct()) - _channelCountryList.postValue(countryCodes) - } - - private fun getChannelsAsync(ids: List): Deferred> = viewModelScope.async(Dispatchers.IO) { - val channels = ArrayList() - - ids.chunked(MAX_LOOKUP_COUNT).forEach { idList -> - val results = repo.getChannelsByIds(idList) - channels.addAll(results) - } - - channels - } - - val bounties: LiveData> - get() = bountyList - - fun filterChannels(countryCode: String): LiveData> { - country = countryCode - val actions = actions.value ?: return MutableLiveData(ArrayList()) - - return if (countryCode == CountryAdapter.CODE_ALL_COUNTRIES) - loadChannels(actions) - else - repo.getChannelsByCountry(getChannelIdArray(actions), countryCode) - } - - private fun getChannelIdArray(actions: List): IntArray = actions.distinctBy { it.channel_id }.map { it.channel_id }.toIntArray() - - private fun makeBountiesIfActions(transactions: List?) { - if (actions.value != null && transactions != null) makeBounties(actions.value, transactions) - } - - private fun makeBounties(actions: List?) { - if (actions != null) makeBounties(actions, transactions.value) - } - - private fun makeBounties(actions: List?, transactions: List?) { - viewModelScope.launch(Dispatchers.Main) { - bountyList.value = getBounties(actions, transactions) - } - } - - private suspend fun getBounties(actions: List?, transactions: List?): MutableList { - coroutineScope { - bountyListAsync = async(Dispatchers.IO) { - val bounties: MutableList = ArrayList() - val transactionsCopy: MutableList = if (transactions == null) ArrayList() else ArrayList(transactions) - for (action in actions!!) { - val filterTransactions: MutableList = ArrayList() - val iter = transactionsCopy.listIterator() - while (iter.hasNext()) { - val t = iter.next() - if (t.action_id == action.public_id) { - filterTransactions.add(t) - iter.remove() - } - } - bounties.add(Bounty(action, filterTransactions)) - } - bounties - } - } - return bountyListAsync.await() - } - - override fun onCleared() { - try { - simReceiver?.let { - LocalBroadcastManager.getInstance(getApplication()).unregisterReceiver(it) - } - } catch (ignored: Exception) { - } - super.onCleared() - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/channels/ImportChannelsWorker.kt b/app/src/main/java/com/hover/stax/channels/ImportChannelsWorker.kt index c32788b16..0b09133c7 100644 --- a/app/src/main/java/com/hover/stax/channels/ImportChannelsWorker.kt +++ b/app/src/main/java/com/hover/stax/channels/ImportChannelsWorker.kt @@ -9,6 +9,7 @@ import androidx.core.app.NotificationCompat import androidx.work.* import com.hover.stax.BuildConfig import com.hover.stax.R +import com.hover.stax.data.local.channels.ChannelDao import com.hover.stax.database.AppDatabase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/app/src/main/java/com/hover/stax/accounts/AccountDao.kt b/app/src/main/java/com/hover/stax/data/local/accounts/AccountDao.kt similarity index 85% rename from app/src/main/java/com/hover/stax/accounts/AccountDao.kt rename to app/src/main/java/com/hover/stax/data/local/accounts/AccountDao.kt index 425aba355..a485db63d 100644 --- a/app/src/main/java/com/hover/stax/accounts/AccountDao.kt +++ b/app/src/main/java/com/hover/stax/data/local/accounts/AccountDao.kt @@ -1,7 +1,8 @@ -package com.hover.stax.accounts +package com.hover.stax.data.local.accounts import androidx.lifecycle.LiveData import androidx.room.* +import com.hover.stax.domain.model.Account import kotlinx.coroutines.flow.Flow @Dao @@ -37,11 +38,14 @@ interface AccountDao { @Query("SELECT * FROM accounts where isDefault = 1") fun getDefaultAccount(): Account? + @Query("SELECT * FROM accounts where isDefault = 1") + suspend fun getDefaultAccountAsync(): Account? + @Query("SELECT COUNT(id) FROM accounts") fun getDataCount(): Int @Insert(onConflict = OnConflictStrategy.IGNORE) - fun insertAll(accounts: List): List + suspend fun insertAll(accounts: List): List @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(account: Account) @@ -50,7 +54,7 @@ interface AccountDao { fun update(account: Account?) @Update - fun updateAll(accounts: List) + suspend fun updateAll(accounts: List) @Delete fun delete(account: Account) diff --git a/app/src/main/java/com/hover/stax/accounts/AccountRepo.kt b/app/src/main/java/com/hover/stax/data/local/accounts/AccountRepo.kt similarity index 84% rename from app/src/main/java/com/hover/stax/accounts/AccountRepo.kt rename to app/src/main/java/com/hover/stax/data/local/accounts/AccountRepo.kt index 1621abe3f..4ca0b91bd 100644 --- a/app/src/main/java/com/hover/stax/accounts/AccountRepo.kt +++ b/app/src/main/java/com/hover/stax/data/local/accounts/AccountRepo.kt @@ -1,7 +1,8 @@ -package com.hover.stax.accounts +package com.hover.stax.data.local.accounts import androidx.lifecycle.LiveData import com.hover.stax.database.AppDatabase +import com.hover.stax.domain.model.Account import com.hover.stax.utils.AnalyticsUtil import kotlinx.coroutines.flow.Flow @@ -18,6 +19,8 @@ class AccountRepo(db: AppDatabase) { fun getDefaultAccount(): Account? = accountDao.getDefaultAccount() + suspend fun getDefaultAccountAsync(): Account? = accountDao.getDefaultAccountAsync() + fun getAccount(id: Int): Account? = accountDao.getAccount(id) fun getLiveAccount(id: Int?): LiveData = accountDao.getLiveAccount(id) @@ -44,10 +47,12 @@ class AccountRepo(db: AppDatabase) { fun insert(account: Account) = accountDao.insert(account) - fun insert(accounts: List): List = accountDao.insertAll(accounts) + suspend fun insert(accounts: List): List = accountDao.insertAll(accounts) fun update(account: Account?) = account?.let { accountDao.update(it) } + suspend fun update(accounts: List) = accountDao.updateAll(accounts) + fun delete(account: Account) = accountDao.delete(account) fun deleteAccount(channelId: Int, name: String) { diff --git a/app/src/main/java/com/hover/stax/actions/ActionRepo.kt b/app/src/main/java/com/hover/stax/data/local/actions/ActionRepo.kt similarity index 92% rename from app/src/main/java/com/hover/stax/actions/ActionRepo.kt rename to app/src/main/java/com/hover/stax/data/local/actions/ActionRepo.kt index a02627d1c..a80dddedd 100644 --- a/app/src/main/java/com/hover/stax/actions/ActionRepo.kt +++ b/app/src/main/java/com/hover/stax/data/local/actions/ActionRepo.kt @@ -1,10 +1,9 @@ -package com.hover.stax.actions +package com.hover.stax.data.local.actions import androidx.lifecycle.LiveData import com.hover.sdk.actions.HoverAction import com.hover.sdk.actions.HoverActionDao import com.hover.sdk.database.HoverRoomDatabase -import com.hover.stax.database.AppDatabase class ActionRepo(sdkDb: HoverRoomDatabase) { @@ -44,4 +43,7 @@ class ActionRepo(sdkDb: HoverRoomDatabase) { val bountyActions: LiveData> get() = actionDao.bountyActions + + val bounties: List + get() = actionDao.bounties } \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/bonus/BonusDao.kt b/app/src/main/java/com/hover/stax/data/local/bonus/BonusDao.kt similarity index 93% rename from app/src/main/java/com/hover/stax/bonus/BonusDao.kt rename to app/src/main/java/com/hover/stax/data/local/bonus/BonusDao.kt index f8c63891b..f015716d3 100644 --- a/app/src/main/java/com/hover/stax/bonus/BonusDao.kt +++ b/app/src/main/java/com/hover/stax/data/local/bonus/BonusDao.kt @@ -1,6 +1,7 @@ -package com.hover.stax.bonus +package com.hover.stax.data.local.bonus import androidx.room.* +import com.hover.stax.domain.model.Bonus import kotlinx.coroutines.flow.Flow @Dao diff --git a/app/src/main/java/com/hover/stax/bonus/BonusRepo.kt b/app/src/main/java/com/hover/stax/data/local/bonus/BonusRepo.kt similarity index 88% rename from app/src/main/java/com/hover/stax/bonus/BonusRepo.kt rename to app/src/main/java/com/hover/stax/data/local/bonus/BonusRepo.kt index d2e01ff5e..f0396a2e7 100644 --- a/app/src/main/java/com/hover/stax/bonus/BonusRepo.kt +++ b/app/src/main/java/com/hover/stax/data/local/bonus/BonusRepo.kt @@ -1,6 +1,7 @@ -package com.hover.stax.bonus +package com.hover.stax.data.local.bonus import com.hover.stax.database.AppDatabase +import com.hover.stax.domain.model.Bonus class BonusRepo(val db: AppDatabase) { diff --git a/app/src/main/java/com/hover/stax/channels/ChannelDao.kt b/app/src/main/java/com/hover/stax/data/local/channels/ChannelDao.kt similarity index 87% rename from app/src/main/java/com/hover/stax/channels/ChannelDao.kt rename to app/src/main/java/com/hover/stax/data/local/channels/ChannelDao.kt index 4dc408904..5f840f0f9 100644 --- a/app/src/main/java/com/hover/stax/channels/ChannelDao.kt +++ b/app/src/main/java/com/hover/stax/data/local/channels/ChannelDao.kt @@ -1,8 +1,9 @@ -package com.hover.stax.channels +package com.hover.stax.data.local.channels import androidx.lifecycle.LiveData import androidx.room.* import com.hover.stax.accounts.ChannelWithAccounts +import com.hover.stax.channels.Channel @Dao interface ChannelDao { @@ -25,8 +26,11 @@ interface ChannelDao { @Query("SELECT * FROM channels WHERE country_alpha2 = :countryCode ORDER BY name ASC") fun getChannels(countryCode: String): List +// @Query("SELECT * FROM channels WHERE country_alpha2 = :countryCode AND id IN (:channel_ids) ORDER BY name ASC") +// fun getChannels(countryCode: String, channel_ids: IntArray): LiveData> + @Query("SELECT * FROM channels WHERE country_alpha2 = :countryCode AND id IN (:channel_ids) ORDER BY name ASC") - fun getChannels(countryCode: String, channel_ids: IntArray): LiveData> + fun getChannels(countryCode: String, channel_ids: IntArray): List @Query("SELECT * FROM channels WHERE id = :id LIMIT 1") fun getChannel(id: Int): Channel? @@ -65,4 +69,5 @@ interface ChannelDao { @Query("DELETE FROM channels") fun deleteAll() + } \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/channels/ChannelRepo.kt b/app/src/main/java/com/hover/stax/data/local/channels/ChannelRepo.kt similarity index 83% rename from app/src/main/java/com/hover/stax/channels/ChannelRepo.kt rename to app/src/main/java/com/hover/stax/data/local/channels/ChannelRepo.kt index f1a410f9e..473df8467 100644 --- a/app/src/main/java/com/hover/stax/channels/ChannelRepo.kt +++ b/app/src/main/java/com/hover/stax/data/local/channels/ChannelRepo.kt @@ -1,11 +1,10 @@ -package com.hover.stax.channels +package com.hover.stax.data.local.channels import androidx.lifecycle.LiveData -import com.hover.sdk.actions.HoverAction import com.hover.sdk.database.HoverRoomDatabase import com.hover.sdk.sims.SimInfo import com.hover.sdk.sims.SimInfoDao -import com.hover.stax.accounts.ChannelWithAccounts +import com.hover.stax.channels.Channel import com.hover.stax.database.AppDatabase class ChannelRepo(db: AppDatabase, sdkDb: HoverRoomDatabase) { @@ -31,9 +30,9 @@ class ChannelRepo(db: AppDatabase, sdkDb: HoverRoomDatabase) { fun getChannelsByIds(ids: List): List = channelDao.getChannelsByIds(ids) - fun getChannelsByCountry(channelIds: IntArray, countryCode: String): LiveData> { - return channelDao.getChannels(countryCode.uppercase(), channelIds) - } + fun getChannelsByIdsAsync(ids: List): List = channelDao.getChannelsByIds(ids) + + fun getChannelsByCountry(channelIds: IntArray, countryCode: String): List = channelDao.getChannels(countryCode, channelIds) fun getChannelsByCountry(countryCode: String): List { return channelDao.getChannels(countryCode.uppercase()) diff --git a/app/src/main/java/com/hover/stax/database/ParserRepo.kt b/app/src/main/java/com/hover/stax/data/local/parser/ParserRepo.kt similarity index 87% rename from app/src/main/java/com/hover/stax/database/ParserRepo.kt rename to app/src/main/java/com/hover/stax/data/local/parser/ParserRepo.kt index 757e05d26..2c3ac6d6c 100644 --- a/app/src/main/java/com/hover/stax/database/ParserRepo.kt +++ b/app/src/main/java/com/hover/stax/data/local/parser/ParserRepo.kt @@ -1,4 +1,4 @@ -package com.hover.stax.database +package com.hover.stax.data.local.parser import com.hover.sdk.database.HoverRoomDatabase import com.hover.sdk.parsers.ParserDao diff --git a/app/src/main/java/com/hover/stax/bounties/UpdateBountyTransactionsWorker.kt b/app/src/main/java/com/hover/stax/data/remote/workers/UpdateBountyTransactionsWorker.kt similarity index 95% rename from app/src/main/java/com/hover/stax/bounties/UpdateBountyTransactionsWorker.kt rename to app/src/main/java/com/hover/stax/data/remote/workers/UpdateBountyTransactionsWorker.kt index 04cf33a7e..1ba2bb3af 100644 --- a/app/src/main/java/com/hover/stax/bounties/UpdateBountyTransactionsWorker.kt +++ b/app/src/main/java/com/hover/stax/data/remote/workers/UpdateBountyTransactionsWorker.kt @@ -1,4 +1,4 @@ -package com.hover.stax.bounties +package com.hover.stax.data.remote.workers import android.content.Context import androidx.work.* @@ -24,8 +24,8 @@ import java.util.concurrent.TimeUnit class UpdateBountyTransactionsWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { companion object { - val TAG = "BountyTransactionWorker" - val BOUNTY_TRANSACTION_WORK_ID = "BOUNTY_TRANSACTION" + const val TAG = "BountyTransactionWorker" + const val BOUNTY_TRANSACTION_WORK_ID = "BOUNTY_TRANSACTION" fun makeToil(): PeriodicWorkRequest { return PeriodicWorkRequest.Builder(UpdateChannelsWorker::class.java, 24, TimeUnit.HOURS) diff --git a/app/src/main/java/com/hover/stax/data/repository/AccountRepositoryImpl.kt b/app/src/main/java/com/hover/stax/data/repository/AccountRepositoryImpl.kt new file mode 100644 index 000000000..df5b728eb --- /dev/null +++ b/app/src/main/java/com/hover/stax/data/repository/AccountRepositoryImpl.kt @@ -0,0 +1,78 @@ +package com.hover.stax.data.repository + +import android.content.Context +import com.hover.sdk.actions.HoverAction +import com.hover.sdk.api.ActionApi +import com.hover.stax.R +import com.hover.stax.data.local.actions.ActionRepo +import com.hover.stax.channels.Channel +import com.hover.stax.data.local.channels.ChannelRepo +import com.hover.stax.data.local.accounts.AccountRepo +import com.hover.stax.domain.model.Account +import com.hover.stax.domain.model.PLACEHOLDER +import com.hover.stax.domain.repository.AccountRepository +import com.hover.stax.notifications.PushNotificationTopicsInterface +import com.hover.stax.utils.AnalyticsUtil +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONObject +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class AccountRepositoryImpl(val accountRepo: AccountRepo, val channelRepo: ChannelRepo, val actionRepo: ActionRepo, private val coroutineDispatcher: CoroutineDispatcher) : AccountRepository, PushNotificationTopicsInterface, KoinComponent { + + private val context: Context by inject() + + override val fetchAccounts: Flow> + get() = accountRepo.getAccounts() + + override suspend fun createAccounts(channels: List): List { + val defaultAccount = accountRepo.getDefaultAccountAsync() + + val accounts = channels.mapIndexed { index, channel -> + val accountName: String = if (getFetchAccountAction(channel.id) == null) channel.name else channel.name.plus(PLACEHOLDER )//placeholder alias for easier identification later + Account( + accountName, channel.name, channel.logoUrl, channel.accountNo, channel.id, channel.countryAlpha2, + channel.id, channel.primaryColorHex, channel.secondaryColorHex, defaultAccount == null && index == 0 + ) + }.onEach { + logChoice(it) + ActionApi.scheduleActionConfigUpdate(it.countryAlpha2, 24, context) + } + + channels.onEach { it.selected = true }.also { channelRepo.update(it) } + return accountRepo.insert(accounts) + } + + override suspend fun setDefaultAccount(account: Account) { + fetchAccounts.collect { accounts -> + val current = accounts.firstOrNull { it.isDefault }?.also { + it.isDefault = false + } + + val defaultAccount = accounts.first { it.id == account.id }.also { it.isDefault = true } + + withContext(coroutineDispatcher) { + launch { + accountRepo.update(listOf(current!!, defaultAccount)) + } + } + } + } + + private fun getFetchAccountAction(channelId: Int): HoverAction? = actionRepo.getActions(channelId, HoverAction.FETCH_ACCOUNTS).firstOrNull() + + private fun logChoice(account: Account) { + joinChannelGroup(account.channelId, context) + val args = JSONObject() + + try { + args.put(context.getString(R.string.added_channel_id), account.channelId) + } catch (ignored: Exception) { + } + + AnalyticsUtil.logAnalyticsEvent(context.getString(R.string.new_channel_selected), args, context) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/data/repository/BonusRepositoryImpl.kt b/app/src/main/java/com/hover/stax/data/repository/BonusRepositoryImpl.kt new file mode 100644 index 000000000..ac2a519c0 --- /dev/null +++ b/app/src/main/java/com/hover/stax/data/repository/BonusRepositoryImpl.kt @@ -0,0 +1,94 @@ +package com.hover.stax.data.repository + +import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.firestore.ktx.firestoreSettings +import com.google.firebase.ktx.Firebase +import com.hover.stax.channels.Channel +import com.hover.stax.data.local.channels.ChannelRepo +import com.hover.stax.data.local.bonus.BonusRepo +import com.hover.stax.domain.model.Bonus +import com.hover.stax.domain.repository.BonusRepository +import com.hover.stax.utils.toHni +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.withContext + +class BonusRepositoryImpl(private val bonusRepo: BonusRepo, private val channelRepo: ChannelRepo, private val coroutineDispatcher: CoroutineDispatcher) : BonusRepository { + + private val settings = firestoreSettings { isPersistenceEnabled = true } + private val db = Firebase.firestore.also { it.firestoreSettings = settings } + + override suspend fun fetchBonuses() { + val bonuses = db.collection("bonuses") + .get() + .await() + .documents + .mapNotNull { document -> + document.data?.let { + Bonus( + it["user_channel"].toString().toInt(), it["purchase_channel"].toString().toInt(), + it["bonus_percent"].toString().toDouble(), it["message"].toString() + ) + } + } + + filterResults(bonuses) + } + + override val bonusList: Flow> + get() = channelFlow { + val simHnis = channelRepo.presentSims.map { it.osReportedHni } + + bonusRepo.bonuses.collect { + withContext(coroutineDispatcher) { + launch { + val bonusChannels = getBonusChannels(it) + val showBonuses = hasValidSim(simHnis, bonusChannels) + + if (showBonuses) + send(it) + else + send(emptyList()) + } + } + } + } + + override suspend fun saveBonuses(bonusList: List) { + return bonusRepo.save(bonusList) + } + + override suspend fun getBonusByPurchaseChannel(channelId: Int): Bonus? { + return bonusRepo.getBonusByPurchaseChannel(channelId) + } + + override suspend fun getBonusByUserChannel(channelId: Int): Bonus? { + return bonusRepo.getBonusByUserChannel(channelId) + } + + private suspend fun filterResults(bonuses: List) = withContext(coroutineDispatcher) { + launch { + val bonusChannels = getBonusChannels(bonuses) + + val toSave = bonuses.filter { bonusChannels.map { channel -> channel.id }.contains(it.purchaseChannel) } + bonusRepo.updateBonuses(toSave) + } + } + + private fun hasValidSim(simHnis: List, bonusChannels: List): Boolean { + val hniList = mutableSetOf() + bonusChannels.forEach { channel -> + channel.hniList.split(",").forEach { + if (simHnis.contains(it.toHni())) + hniList.add(it.toHni()) + } + } + + return hniList.isNotEmpty() + } + + override suspend fun getBonusChannels(bonusList: List): List = channelRepo.getChannelsByIdsAsync(bonusList.map { it.purchaseChannel }) +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/data/repository/BountyRepositoryImpl.kt b/app/src/main/java/com/hover/stax/data/repository/BountyRepositoryImpl.kt new file mode 100644 index 000000000..f5ca56ca9 --- /dev/null +++ b/app/src/main/java/com/hover/stax/data/repository/BountyRepositoryImpl.kt @@ -0,0 +1,76 @@ +package com.hover.stax.data.repository + +import androidx.lifecycle.LiveData +import com.hover.sdk.actions.HoverAction +import com.hover.sdk.sims.SimInfo +import com.hover.stax.channels.Channel +import com.hover.stax.countries.CountryAdapter +import com.hover.stax.data.local.actions.ActionRepo +import com.hover.stax.domain.model.Bounty +import com.hover.stax.domain.model.ChannelBounties +import com.hover.stax.domain.repository.BountyRepository +import com.hover.stax.transactions.StaxTransaction +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.launch + +class BountyRepositoryImpl(val actionRepo: ActionRepo, private val coroutineDispatcher: CoroutineDispatcher) : BountyRepository { + + override val bountyActions: List + get() = actionRepo.bounties + + override fun isSimPresent(bounty: Bounty, sims: List): Boolean { + if (sims.isEmpty()) return false + + sims.forEach { simInfo -> + for (i in 0 until bounty.action.hni_list.length()) if (bounty.action.hni_list.optString(i) == simInfo.osReportedHni) return true + } + + return false + } + + override fun getCountryList(): Flow> = channelFlow { + launch(coroutineDispatcher) { + val actions = bountyActions + val countryCodes = mutableListOf(CountryAdapter.CODE_ALL_COUNTRIES) + actions.asSequence().map { it.country_alpha2.uppercase() }.distinct().sorted().toCollection(countryCodes) + send(countryCodes) + } + } + + override suspend fun makeBounties(actions: List, transactions: List?, channels: List): List { + if (actions.isEmpty()) return emptyList() + + val bounties = getBounties(actions, transactions) + + return generateChannelBounties(channels, bounties) + } + + private fun getBounties(actions: List, transactions: List?): List { + val bounties: MutableList = ArrayList() + val transactionList = transactions?.toMutableList() ?: mutableListOf() + + for (action in actions) { + val filteredTransactions = transactionList.filter { it.action_id == action.public_id } + bounties.add(Bounty(action, filteredTransactions)) + } + + return bounties + } + + private fun generateChannelBounties(channels: List, bounties: List): List { + if (channels.isEmpty() || bounties.isEmpty()) return emptyList() + + val openBounties = bounties.filter { it.action.bounty_is_open || it.transactionCount != 0 } + + val channelBounties = channels.filter { c -> + openBounties.any { it.action.channel_id == c.id } + }.map { channel -> + ChannelBounties(channel, openBounties.filter { it.action.channel_id == channel.id }) + } + + return channelBounties + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/data/repository/ChannelRepositoryImpl.kt b/app/src/main/java/com/hover/stax/data/repository/ChannelRepositoryImpl.kt new file mode 100644 index 000000000..430c7fbfd --- /dev/null +++ b/app/src/main/java/com/hover/stax/data/repository/ChannelRepositoryImpl.kt @@ -0,0 +1,41 @@ +package com.hover.stax.data.repository + +import com.hover.sdk.actions.HoverAction +import com.hover.sdk.sims.SimInfo +import com.hover.stax.channels.Channel +import com.hover.stax.countries.CountryAdapter +import com.hover.stax.data.local.channels.ChannelRepo +import com.hover.stax.domain.repository.ChannelRepository + +private const val MAX_LOOKUP_COUNT = 40 + +class ChannelRepositoryImpl(val channelRepo: ChannelRepo) : ChannelRepository { + + override val presentSims: List + get() = channelRepo.presentSims + + override suspend fun getChannelsByIds(ids: List): List = channelRepo.getChannelsByIds(ids) + + override suspend fun getChannelsByCountryCode(ids: IntArray, countryCode: String): List = channelRepo.getChannelsByCountry(ids, countryCode) + + override suspend fun filterChannels(countryCode: String, actions: List): List { + val ids = actions.asSequence().distinctBy { it.channel_id }.map { it.channel_id }.toList() + + return if (countryCode == CountryAdapter.CODE_ALL_COUNTRIES) + getChunkedChannelsByIds(ids) + else + getChannelsByCountryCode(ids.toIntArray(), countryCode) + } + + private fun getChunkedChannelsByIds(ids: List): List { + val channels = mutableListOf() + + ids.chunked(MAX_LOOKUP_COUNT).forEach { idList -> + val results = channelRepo.getChannelsByIds(idList) + channels.addAll(results) + } + + return channels + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/data/repository/FinancialTipsRepositoryImpl.kt b/app/src/main/java/com/hover/stax/data/repository/FinancialTipsRepositoryImpl.kt new file mode 100644 index 000000000..61d8d426d --- /dev/null +++ b/app/src/main/java/com/hover/stax/data/repository/FinancialTipsRepositoryImpl.kt @@ -0,0 +1,48 @@ +package com.hover.stax.data.repository + +import android.content.Context +import com.google.firebase.firestore.Query +import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.firestore.ktx.firestoreSettings +import com.google.firebase.ktx.Firebase +import com.hover.stax.R +import com.hover.stax.domain.model.FINANCIAL_TIP_ID +import com.hover.stax.domain.model.FinancialTip +import com.hover.stax.domain.repository.FinancialTipsRepository +import com.hover.stax.utils.Utils +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.tasks.await + +class FinancialTipsRepositoryImpl(val context: Context) : FinancialTipsRepository { + + val db = Firebase.firestore + val settings = firestoreSettings { isPersistenceEnabled = true } + + override suspend fun getTips(): List { + val today = System.currentTimeMillis() + + return db.collection(context.getString(R.string.tips_table)) + .orderBy("date", Query.Direction.DESCENDING) + .whereLessThanOrEqualTo("date", today / 1000) + .limit(20) + .get() + .await() + .documents + .mapNotNull { document -> + FinancialTip( + document.id, document.data!!["title"].toString(), document.data!!["content"].toString(), + document.data!!["snippet"].toString(), (document.data!!["date"].toString().toLong() * 1000), document.data!!["share copy"].toString(), + document.data!!["deep link"].toString() + ) + } + } + + override fun getDismissedTipId(): String? { + return Utils.getString(FINANCIAL_TIP_ID, context) + } + + override fun dismissTip(id: String) { + Utils.saveString(FINANCIAL_TIP_ID, id, context) + } +} diff --git a/app/src/main/java/com/hover/stax/database/AppDatabase.kt b/app/src/main/java/com/hover/stax/database/AppDatabase.kt index 5223c5a5e..fa425606a 100644 --- a/app/src/main/java/com/hover/stax/database/AppDatabase.kt +++ b/app/src/main/java/com/hover/stax/database/AppDatabase.kt @@ -6,12 +6,12 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.migration.Migration -import com.hover.stax.accounts.Account -import com.hover.stax.accounts.AccountDao -import com.hover.stax.bonus.Bonus -import com.hover.stax.bonus.BonusDao +import com.hover.stax.domain.model.Account +import com.hover.stax.data.local.accounts.AccountDao +import com.hover.stax.domain.model.Bonus +import com.hover.stax.data.local.bonus.BonusDao import com.hover.stax.channels.Channel -import com.hover.stax.channels.ChannelDao +import com.hover.stax.data.local.channels.ChannelDao import com.hover.stax.contacts.ContactDao import com.hover.stax.contacts.StaxContact import com.hover.stax.merchants.Merchant diff --git a/app/src/main/java/com/hover/stax/database/Modules.kt b/app/src/main/java/com/hover/stax/database/Modules.kt deleted file mode 100644 index 21e0daa9a..000000000 --- a/app/src/main/java/com/hover/stax/database/Modules.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.hover.stax.database - -import com.hover.sdk.database.HoverRoomDatabase -import com.hover.stax.accounts.AccountDetailViewModel -import com.hover.stax.accounts.AccountRepo -import com.hover.stax.actions.ActionRepo -import com.hover.stax.actions.ActionSelectViewModel -import com.hover.stax.addChannels.ChannelsViewModel -import com.hover.stax.balances.BalancesViewModel -import com.hover.stax.bonus.BonusRepo -import com.hover.stax.bonus.BonusViewModel -import com.hover.stax.bounties.BountyViewModel -import com.hover.stax.channels.ChannelRepo -import com.hover.stax.accounts.AccountsViewModel -import com.hover.stax.contacts.ContactRepo -import com.hover.stax.faq.FaqViewModel -import com.hover.stax.financialTips.FinancialTipsViewModel -import com.hover.stax.futureTransactions.FutureViewModel -import com.hover.stax.inapp_banner.BannerViewModel -import com.hover.stax.languages.LanguageViewModel -import com.hover.stax.login.LoginNetworking -import com.hover.stax.login.LoginViewModel -import com.hover.stax.merchants.MerchantRepo -import com.hover.stax.merchants.MerchantViewModel -import com.hover.stax.paybill.PaybillRepo -import com.hover.stax.paybill.PaybillViewModel -import com.hover.stax.requests.NewRequestViewModel -import com.hover.stax.requests.RequestDetailViewModel -import com.hover.stax.requests.RequestRepo -import com.hover.stax.schedules.ScheduleDetailViewModel -import com.hover.stax.schedules.ScheduleRepo -import com.hover.stax.transactionDetails.TransactionDetailsViewModel -import com.hover.stax.transactions.TransactionHistoryViewModel -import com.hover.stax.transactions.TransactionRepo -import com.hover.stax.transfers.TransferViewModel -import com.hover.stax.user.UserRepo -import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.dsl.module - -val appModule = module { - viewModel { FaqViewModel() } - viewModel { ActionSelectViewModel(get()) } - viewModel { ChannelsViewModel(get(), get(), get(), get(), get()) } - viewModel { AccountsViewModel(get(), get(), get(), get()) } - viewModel { AccountDetailViewModel(get(), get(), get(), get(), get()) } - viewModel { NewRequestViewModel(get(), get(), get(), get(), get()) } - viewModel { TransferViewModel(get(), get(), get(), get()) } - viewModel { ScheduleDetailViewModel(get(), get(), get()) } - viewModel { BalancesViewModel(get(), get(), get()) } - viewModel { TransactionHistoryViewModel(get(), get()) } - viewModel { BannerViewModel(get(), get()) } - viewModel { FutureViewModel(get(), get(), get()) } - viewModel { LoginViewModel(get(), get(), get())} - viewModel { TransactionDetailsViewModel(get(), get(), get(), get(), get(), get(), get(), get()) } - viewModel { LanguageViewModel(get()) } - viewModel { BountyViewModel(get(), get(), get(), get()) } - viewModel { FinancialTipsViewModel(get()) } - viewModel { PaybillViewModel(get(), get(), get(), get(), get(), get()) } - viewModel { MerchantViewModel(get(), get(), get(), get()) } - viewModel { RequestDetailViewModel(get(), get(), get()) } - viewModel { BonusViewModel(get(), get()) } -} - -val dataModule = module(createdAtStart = true) { - single { AppDatabase.getInstance(get()) } - single { HoverRoomDatabase.getInstance(get()) } - - single { TransactionRepo(get()) } - single { ChannelRepo(get(), get()) } - single { ActionRepo(get()) } - single { ContactRepo(get()) } - single { AccountRepo(get()) } - single { RequestRepo(get()) } - single { ScheduleRepo(get()) } - single { PaybillRepo(get()) } - single { MerchantRepo(get()) } - single { UserRepo(get()) } - single { BonusRepo(get()) } - single { ParserRepo(get()) } -} - -val networkModule = module { - single { LoginNetworking(get()) } -} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/di/Modules.kt b/app/src/main/java/com/hover/stax/di/Modules.kt new file mode 100644 index 000000000..ea940a0ed --- /dev/null +++ b/app/src/main/java/com/hover/stax/di/Modules.kt @@ -0,0 +1,131 @@ +package com.hover.stax.di + +import com.hover.sdk.database.HoverRoomDatabase +import com.hover.stax.accounts.AccountDetailViewModel +import com.hover.stax.accounts.AccountsViewModel +import com.hover.stax.actions.ActionSelectViewModel +import com.hover.stax.addChannels.ChannelsViewModel +import com.hover.stax.bonus.BonusViewModel +import com.hover.stax.contacts.ContactRepo +import com.hover.stax.data.local.accounts.AccountRepo +import com.hover.stax.data.local.actions.ActionRepo +import com.hover.stax.data.local.bonus.BonusRepo +import com.hover.stax.data.local.channels.ChannelRepo +import com.hover.stax.data.local.parser.ParserRepo +import com.hover.stax.data.repository.* +import com.hover.stax.database.AppDatabase +import com.hover.stax.domain.repository.* +import com.hover.stax.domain.use_case.accounts.CreateAccountsUseCase +import com.hover.stax.domain.use_case.accounts.GetAccountsUseCase +import com.hover.stax.domain.use_case.accounts.SetDefaultAccountUseCase +import com.hover.stax.domain.use_case.bonus.FetchBonusUseCase +import com.hover.stax.domain.use_case.bonus.GetBonusesUseCase +import com.hover.stax.domain.use_case.bounties.GetChannelBountiesUseCase +import com.hover.stax.domain.use_case.channels.GetPresentSimsUseCase +import com.hover.stax.domain.use_case.financial_tips.TipsUseCase +import com.hover.stax.faq.FaqViewModel +import com.hover.stax.futureTransactions.FutureViewModel +import com.hover.stax.inapp_banner.BannerViewModel +import com.hover.stax.languages.LanguageViewModel +import com.hover.stax.login.LoginNetworking +import com.hover.stax.login.LoginViewModel +import com.hover.stax.merchants.MerchantRepo +import com.hover.stax.merchants.MerchantViewModel +import com.hover.stax.paybill.PaybillRepo +import com.hover.stax.paybill.PaybillViewModel +import com.hover.stax.presentation.bounties.BountyViewModel +import com.hover.stax.presentation.financial_tips.FinancialTipsViewModel +import com.hover.stax.presentation.home.BalancesViewModel +import com.hover.stax.presentation.home.HomeViewModel +import com.hover.stax.requests.NewRequestViewModel +import com.hover.stax.requests.RequestDetailViewModel +import com.hover.stax.requests.RequestRepo +import com.hover.stax.schedules.ScheduleDetailViewModel +import com.hover.stax.schedules.ScheduleRepo +import com.hover.stax.transactionDetails.TransactionDetailsViewModel +import com.hover.stax.transactions.TransactionHistoryViewModel +import com.hover.stax.transactions.TransactionRepo +import com.hover.stax.transfers.TransferViewModel +import com.hover.stax.user.UserRepo +import kotlinx.coroutines.Dispatchers +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.factoryOf +import org.koin.core.module.dsl.singleOf +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val appModule = module { + viewModelOf(::FaqViewModel) + viewModelOf(::ActionSelectViewModel) + viewModelOf(::ChannelsViewModel) + viewModelOf(::AccountsViewModel) + viewModelOf(::AccountDetailViewModel) + viewModelOf(::NewRequestViewModel) + viewModelOf(::TransferViewModel) + viewModelOf(::ScheduleDetailViewModel) + viewModelOf(::BalancesViewModel) + viewModelOf(::TransactionHistoryViewModel) + viewModelOf(::BannerViewModel) + viewModelOf(::FutureViewModel) + viewModelOf(::LoginViewModel) + viewModelOf(::TransactionDetailsViewModel) + viewModelOf(::LanguageViewModel) + viewModelOf(::BountyViewModel) + viewModelOf(::FinancialTipsViewModel) + viewModelOf(::PaybillViewModel) + viewModelOf(::MerchantViewModel) + viewModelOf(::RequestDetailViewModel) + viewModelOf(::BonusViewModel) + + viewModelOf(::HomeViewModel) +} + +val dataModule = module(createdAtStart = true) { + single { AppDatabase.getInstance(get()) } + single { HoverRoomDatabase.getInstance(get()) } + + singleOf(::TransactionRepo) + singleOf(::ChannelRepo) + singleOf(::ActionRepo) + singleOf(::ContactRepo) + singleOf(::AccountRepo) + singleOf(::RequestRepo) + singleOf(::ScheduleRepo) + singleOf(::PaybillRepo) + singleOf(::MerchantRepo) + singleOf(::UserRepo) + singleOf(::BonusRepo) + singleOf(::ParserRepo) +} + +val networkModule = module { + singleOf(::LoginNetworking) +} + +val repositories = module { + single(named("CoroutineDispatcher")) { + Dispatchers.IO + } + + single { BonusRepositoryImpl(get(), get(), get(named("CoroutineDispatcher"))) } + single { AccountRepositoryImpl(get(), get(), get(), get(named("CoroutineDispatcher"))) } + single { BountyRepositoryImpl(get(), get(named("CoroutineDispatcher"))) } + + singleOf(::FinancialTipsRepositoryImpl) { bind() } + singleOf(::ChannelRepositoryImpl) { bind() } +} + +val useCases = module { + factoryOf(::GetBonusesUseCase) + factoryOf(::FetchBonusUseCase) + + factoryOf(::GetAccountsUseCase) + factoryOf(::SetDefaultAccountUseCase) + factoryOf(::CreateAccountsUseCase) + + factoryOf(::TipsUseCase) + + factoryOf(::GetChannelBountiesUseCase) + factoryOf(::GetPresentSimsUseCase) +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/accounts/Account.kt b/app/src/main/java/com/hover/stax/domain/model/Account.kt similarity index 92% rename from app/src/main/java/com/hover/stax/accounts/Account.kt rename to app/src/main/java/com/hover/stax/domain/model/Account.kt index 9c6670637..f385fa61d 100644 --- a/app/src/main/java/com/hover/stax/accounts/Account.kt +++ b/app/src/main/java/com/hover/stax/domain/model/Account.kt @@ -1,12 +1,11 @@ -package com.hover.stax.accounts +package com.hover.stax.domain.model import androidx.room.* import com.hover.stax.channels.Channel import com.hover.stax.utils.DateUtils.now import timber.log.Timber -const val DUMMY = -1 -const val PLACEHOLDER = "placeholder" +const val PLACEHOLDER = " placeholder" const val ACCOUNT_NAME: String = "account_name" const val ACCOUNT_ID: String = "account_id" @@ -76,13 +75,6 @@ data class Account( } } - fun dummy(): Account { - id = DUMMY - latestBalanceTimestamp = -1L - latestBalance = "0" - return this - } - override fun toString() = buildString { append(alias) diff --git a/app/src/main/java/com/hover/stax/bonus/Bonus.kt b/app/src/main/java/com/hover/stax/domain/model/Bonus.kt similarity index 93% rename from app/src/main/java/com/hover/stax/bonus/Bonus.kt rename to app/src/main/java/com/hover/stax/domain/model/Bonus.kt index adef25845..5f0837080 100644 --- a/app/src/main/java/com/hover/stax/bonus/Bonus.kt +++ b/app/src/main/java/com/hover/stax/domain/model/Bonus.kt @@ -1,4 +1,4 @@ -package com.hover.stax.bonus +package com.hover.stax.domain.model import androidx.room.ColumnInfo import androidx.room.Entity diff --git a/app/src/main/java/com/hover/stax/bounties/Bounty.kt b/app/src/main/java/com/hover/stax/domain/model/Bounty.kt similarity index 94% rename from app/src/main/java/com/hover/stax/bounties/Bounty.kt rename to app/src/main/java/com/hover/stax/domain/model/Bounty.kt index 53bb4201f..30a5a5a11 100644 --- a/app/src/main/java/com/hover/stax/bounties/Bounty.kt +++ b/app/src/main/java/com/hover/stax/domain/model/Bounty.kt @@ -1,4 +1,4 @@ -package com.hover.stax.bounties +package com.hover.stax.domain.model import android.content.Context import com.hover.sdk.actions.HoverAction @@ -9,13 +9,11 @@ import com.hover.stax.transactions.StaxTransaction import com.yariksoffice.lingver.Lingver import java.util.* - class Bounty(val action: HoverAction, val transactions: List) { val transactionCount get(): Int = transactions.size - - fun lastTransactionIndex(): Int = if (transactionCount == 0) 0 else transactionCount - 1 - fun hasASuccessfulTransaction(): Boolean = transactions.any { it.status == Transaction.SUCCEEDED } + + fun hasSuccessfulTransactions(): Boolean = transactions.any { it.status == Transaction.SUCCEEDED } fun isLastTransactionFailed(): Boolean = if (transactionCount == 0) false else transactions.last().status == Transaction.FAILED fun generateDescription(c: Context): String = when (action.transaction_type) { diff --git a/app/src/main/java/com/hover/stax/domain/model/FinancialTip.kt b/app/src/main/java/com/hover/stax/domain/model/FinancialTip.kt new file mode 100644 index 000000000..acaf7a485 --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/model/FinancialTip.kt @@ -0,0 +1,12 @@ +package com.hover.stax.domain.model + +data class FinancialTip( + val id: String, + val title: String, + val content: String, + val snippet: String, + val date: Long?, + val shareCopy: String?, + val deepLink: String? +) +val FINANCIAL_TIP_ID = "id" diff --git a/app/src/main/java/com/hover/stax/domain/model/Resource.kt b/app/src/main/java/com/hover/stax/domain/model/Resource.kt new file mode 100644 index 000000000..6d10e9be7 --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/model/Resource.kt @@ -0,0 +1,7 @@ +package com.hover.stax.domain.model + +sealed class Resource(val data: T? = null, val message: String? = null) { + class Success(data: T) : Resource(data) + class Error(message: String, data: T? = null) : Resource(data, message) + class Loading(data: T? = null) : Resource(data) +} diff --git a/app/src/main/java/com/hover/stax/domain/repository/AccountRepository.kt b/app/src/main/java/com/hover/stax/domain/repository/AccountRepository.kt new file mode 100644 index 000000000..bac9cbfdf --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/repository/AccountRepository.kt @@ -0,0 +1,14 @@ +package com.hover.stax.domain.repository + +import com.hover.stax.domain.model.Account +import com.hover.stax.channels.Channel +import kotlinx.coroutines.flow.Flow + +interface AccountRepository { + + val fetchAccounts: Flow> + + suspend fun createAccounts(channels: List): List + + suspend fun setDefaultAccount(account: Account) +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/domain/repository/BonusRepository.kt b/app/src/main/java/com/hover/stax/domain/repository/BonusRepository.kt new file mode 100644 index 000000000..f5b21cd61 --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/repository/BonusRepository.kt @@ -0,0 +1,20 @@ +package com.hover.stax.domain.repository + +import com.hover.stax.domain.model.Bonus +import com.hover.stax.channels.Channel +import kotlinx.coroutines.flow.Flow + +interface BonusRepository { + + suspend fun fetchBonuses() + + val bonusList: Flow> + + suspend fun saveBonuses(bonusList: List) + + suspend fun getBonusChannels(bonusList: List): List + + suspend fun getBonusByPurchaseChannel(channelId: Int): Bonus? + + suspend fun getBonusByUserChannel(channelId: Int): Bonus? +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/domain/repository/BountyRepository.kt b/app/src/main/java/com/hover/stax/domain/repository/BountyRepository.kt new file mode 100644 index 000000000..d6ccafc6e --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/repository/BountyRepository.kt @@ -0,0 +1,21 @@ +package com.hover.stax.domain.repository + +import androidx.lifecycle.LiveData +import com.hover.sdk.actions.HoverAction +import com.hover.sdk.sims.SimInfo +import com.hover.stax.domain.model.Bounty +import com.hover.stax.domain.model.ChannelBounties +import com.hover.stax.channels.Channel +import com.hover.stax.transactions.StaxTransaction +import kotlinx.coroutines.flow.Flow + +interface BountyRepository { + + val bountyActions: List + + fun isSimPresent(bounty: Bounty, sims: List): Boolean + + fun getCountryList(): Flow> + + suspend fun makeBounties(actions: List, transactions: List?, channels: List): List +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/domain/repository/ChannelRepository.kt b/app/src/main/java/com/hover/stax/domain/repository/ChannelRepository.kt new file mode 100644 index 000000000..0429cd5f4 --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/repository/ChannelRepository.kt @@ -0,0 +1,17 @@ +package com.hover.stax.domain.repository + +import com.hover.sdk.actions.HoverAction +import com.hover.sdk.sims.SimInfo +import com.hover.stax.channels.Channel +import kotlinx.coroutines.flow.Flow + +interface ChannelRepository { + + val presentSims: List + + suspend fun getChannelsByIds(ids: List): List + + suspend fun getChannelsByCountryCode(ids: IntArray, countryCode: String): List + + suspend fun filterChannels(countryCode: String, actions: List): List +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/domain/repository/FinancialTipsRepository.kt b/app/src/main/java/com/hover/stax/domain/repository/FinancialTipsRepository.kt new file mode 100644 index 000000000..70990c007 --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/repository/FinancialTipsRepository.kt @@ -0,0 +1,11 @@ +package com.hover.stax.domain.repository + +import com.hover.stax.domain.model.FinancialTip +import kotlinx.coroutines.flow.Flow + +interface FinancialTipsRepository { + + suspend fun getTips(): List + fun getDismissedTipId() : String? + fun dismissTip(id: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/domain/use_case/accounts/CreateAccountsUseCase.kt b/app/src/main/java/com/hover/stax/domain/use_case/accounts/CreateAccountsUseCase.kt new file mode 100644 index 000000000..ba435cdd7 --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/use_case/accounts/CreateAccountsUseCase.kt @@ -0,0 +1,11 @@ +package com.hover.stax.domain.use_case.accounts + +import com.hover.stax.channels.Channel +import com.hover.stax.domain.repository.AccountRepository + +class CreateAccountsUseCase(private val accountsRepository: AccountRepository) { + + suspend operator fun invoke(channels: List): List { + return accountsRepository.createAccounts(channels) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/domain/use_case/accounts/GetAccountsUseCase.kt b/app/src/main/java/com/hover/stax/domain/use_case/accounts/GetAccountsUseCase.kt new file mode 100644 index 000000000..48ee55ba4 --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/use_case/accounts/GetAccountsUseCase.kt @@ -0,0 +1,11 @@ +package com.hover.stax.domain.use_case.accounts + +import com.hover.stax.domain.model.Account +import com.hover.stax.domain.repository.AccountRepository +import kotlinx.coroutines.flow.Flow + +class GetAccountsUseCase(accountsRepository: AccountRepository) { + + val accounts: Flow> = accountsRepository.fetchAccounts + +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/domain/use_case/accounts/SetDefaultAccountUseCase.kt b/app/src/main/java/com/hover/stax/domain/use_case/accounts/SetDefaultAccountUseCase.kt new file mode 100644 index 000000000..1c2b91651 --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/use_case/accounts/SetDefaultAccountUseCase.kt @@ -0,0 +1,11 @@ +package com.hover.stax.domain.use_case.accounts + +import com.hover.stax.domain.model.Account +import com.hover.stax.domain.repository.AccountRepository + +class SetDefaultAccountUseCase(private val accountsRepository: AccountRepository) { + + suspend operator fun invoke(account: Account) { + accountsRepository.setDefaultAccount(account) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/domain/use_case/bonus/FetchBonusUseCase.kt b/app/src/main/java/com/hover/stax/domain/use_case/bonus/FetchBonusUseCase.kt new file mode 100644 index 000000000..7c2e58cc0 --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/use_case/bonus/FetchBonusUseCase.kt @@ -0,0 +1,9 @@ +package com.hover.stax.domain.use_case.bonus + +import com.hover.stax.domain.repository.BonusRepository + +class FetchBonusUseCase(private val repository: BonusRepository) { + + suspend operator fun invoke() = repository.fetchBonuses() + +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/domain/use_case/bonus/GetBonusesUseCase.kt b/app/src/main/java/com/hover/stax/domain/use_case/bonus/GetBonusesUseCase.kt new file mode 100644 index 000000000..a3262669a --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/use_case/bonus/GetBonusesUseCase.kt @@ -0,0 +1,11 @@ +package com.hover.stax.domain.use_case.bonus + +import com.hover.stax.domain.model.Bonus +import com.hover.stax.domain.repository.BonusRepository +import kotlinx.coroutines.flow.Flow + +class GetBonusesUseCase(repository: BonusRepository) { + + val bonusList: Flow> = repository.bonusList + +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/domain/use_case/bounties/GetChannelBountiesUseCase.kt b/app/src/main/java/com/hover/stax/domain/use_case/bounties/GetChannelBountiesUseCase.kt new file mode 100644 index 000000000..201e6bfe6 --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/use_case/bounties/GetChannelBountiesUseCase.kt @@ -0,0 +1,45 @@ +package com.hover.stax.domain.use_case.bounties + +import com.hover.stax.countries.CountryAdapter +import com.hover.stax.domain.model.ChannelBounties +import com.hover.stax.domain.model.Resource +import com.hover.stax.domain.repository.BountyRepository +import com.hover.stax.domain.repository.ChannelRepository +import com.hover.stax.transactions.TransactionRepo +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class GetChannelBountiesUseCase(private val channelRepository: ChannelRepository, private val bountyRepository: BountyRepository, private val transactionRepo: TransactionRepo) { + + fun getBounties(countryCode: String = CountryAdapter.CODE_ALL_COUNTRIES): Flow>> = flow { + try { + emit(Resource.Loading()) + + emit(Resource.Success(fetchBounties(countryCode))) + } catch (e: Exception) { + emit(Resource.Error("Error loading bounties")) + } + } + + private suspend fun fetchBounties(countryCode: String): List { + val channelBounties: Deferred> + + coroutineScope { + channelBounties = async(Dispatchers.IO) { + val bountyActions = bountyRepository.bountyActions + val bountyTransactionList = transactionRepo.bountyTransactionList + val bountyChannels = channelRepository.filterChannels(countryCode, bountyActions) + + bountyRepository.makeBounties(bountyActions, bountyTransactionList, bountyChannels) + } + } + + return channelBounties.await() + } + + fun getChannelList(): Flow> = bountyRepository.getCountryList() +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/domain/use_case/channels/GetPresentSimsUseCase.kt b/app/src/main/java/com/hover/stax/domain/use_case/channels/GetPresentSimsUseCase.kt new file mode 100644 index 000000000..8fdb87096 --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/use_case/channels/GetPresentSimsUseCase.kt @@ -0,0 +1,14 @@ +package com.hover.stax.domain.use_case.channels + +import com.hover.sdk.sims.SimInfo +import com.hover.stax.domain.model.Bounty +import com.hover.stax.domain.repository.BountyRepository +import com.hover.stax.domain.repository.ChannelRepository + +class GetPresentSimsUseCase(channelRepository: ChannelRepository, private val bountyRepository: BountyRepository) { + + val presentSims: List = channelRepository.presentSims + + fun simPresent(bounty: Bounty, sims: List): Boolean = bountyRepository.isSimPresent(bounty, sims) + +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/domain/use_case/financial_tips/TipsUseCase.kt b/app/src/main/java/com/hover/stax/domain/use_case/financial_tips/TipsUseCase.kt new file mode 100644 index 000000000..cc1601d2f --- /dev/null +++ b/app/src/main/java/com/hover/stax/domain/use_case/financial_tips/TipsUseCase.kt @@ -0,0 +1,27 @@ +package com.hover.stax.domain.use_case.financial_tips + +import androidx.compose.runtime.mutableStateOf +import com.hover.stax.domain.model.FinancialTip +import com.hover.stax.domain.model.Resource +import com.hover.stax.domain.repository.FinancialTipsRepository +import kotlinx.coroutines.flow.* +import timber.log.Timber + +class TipsUseCase(private val financialTipsRepository: FinancialTipsRepository) { + + operator fun invoke(): Flow>> = flow { + try { + emit(Resource.Loading()) + + val financialTips = financialTipsRepository.getTips() + emit(Resource.Success(financialTips)) + } catch (e: Exception) { + emit(Resource.Error("Error fetching tips")) + } + } + + fun getDismissedTipId() : String? = financialTipsRepository.getDismissedTipId() + fun dismissTip(id: String) { + financialTipsRepository.dismissTip(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/financialTips/FinancialTipsViewModel.kt b/app/src/main/java/com/hover/stax/financialTips/FinancialTipsViewModel.kt deleted file mode 100644 index e9e573bc1..000000000 --- a/app/src/main/java/com/hover/stax/financialTips/FinancialTipsViewModel.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.hover.stax.financialTips - -import android.app.Application -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.google.firebase.firestore.Query -import com.google.firebase.firestore.ktx.firestore -import com.google.firebase.firestore.ktx.firestoreSettings -import com.google.firebase.ktx.Firebase -import com.hover.stax.R -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import timber.log.Timber - -data class FinancialTip(val id: String, val title: String, val content: String, val snippet: String, val date: Long?, val shareCopy: String?, val deepLink: String?) - -data class FinancialTipsState(val tips: List = emptyList()) - -class FinancialTipsViewModel(val application: Application) : ViewModel() { - - val db = Firebase.firestore - val settings = firestoreSettings { isPersistenceEnabled = true } - - private val _tips = MutableStateFlow(FinancialTipsState()) - val tipState: StateFlow = _tips - - init { - db.firestoreSettings = settings - } - - fun getTips() = viewModelScope.launch { - val today = System.currentTimeMillis() - - db.collection(application.getString(R.string.tips_table)) - .orderBy("date", Query.Direction.DESCENDING) - .whereLessThanOrEqualTo("date", today / 1000) - .limit(20) - .get() - .addOnSuccessListener { snapshot -> - val financialTip = snapshot.map { document -> - FinancialTip( - document.id, document.data["title"].toString(), document.data["content"].toString(), - document.data["snippet"].toString(), (document.data["date"].toString().toLong() * 1000), document.data["share copy"].toString(), - document.data["deep link"].toString() - ) - } - - _tips.value = tipState.value.copy(tips = financialTip.filterNot { it.date == null }.sortedByDescending { it.date }) - } - .addOnFailureListener { - Timber.e("Error fetching wellness tips: ${it.localizedMessage}") - _tips.value = tipState.value.copy(tips = emptyList()) - } - } -} diff --git a/app/src/main/java/com/hover/stax/futureTransactions/FutureViewModel.kt b/app/src/main/java/com/hover/stax/futureTransactions/FutureViewModel.kt index d48ab4379..13db61178 100644 --- a/app/src/main/java/com/hover/stax/futureTransactions/FutureViewModel.kt +++ b/app/src/main/java/com/hover/stax/futureTransactions/FutureViewModel.kt @@ -5,12 +5,11 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.hover.stax.channels.Channel -import com.hover.stax.channels.ChannelRepo +import com.hover.stax.data.local.channels.ChannelRepo import com.hover.stax.schedules.ScheduleRepo import com.hover.stax.requests.Request import com.hover.stax.requests.RequestRepo import com.hover.stax.schedules.Schedule -import com.hover.stax.transactions.TransactionRepo import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async diff --git a/app/src/main/java/com/hover/stax/home/HomeFragment.kt b/app/src/main/java/com/hover/stax/home/HomeFragment.kt deleted file mode 100644 index f55dd2c77..000000000 --- a/app/src/main/java/com/hover/stax/home/HomeFragment.kt +++ /dev/null @@ -1,159 +0,0 @@ -package com.hover.stax.home - -import android.os.Bundle -import android.text.method.LinkMovementMethod -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.NavDirections -import androidx.navigation.fragment.findNavController -import com.hover.sdk.actions.HoverAction -import com.hover.stax.R -import com.hover.stax.addChannels.ChannelsViewModel -import com.hover.stax.bonus.Bonus -import com.hover.stax.bonus.BonusViewModel -import com.hover.stax.databinding.FragmentHomeBinding -import com.hover.stax.financialTips.FinancialTip -import com.hover.stax.financialTips.FinancialTipsViewModel -import com.hover.stax.utils.AnalyticsUtil -import com.hover.stax.utils.NavUtil -import com.hover.stax.utils.collectLatestLifecycleFlow -import com.hover.stax.utils.network.NetworkMonitor -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch -import org.koin.androidx.viewmodel.ext.android.sharedViewModel -import org.koin.androidx.viewmodel.ext.android.viewModel -import timber.log.Timber -import java.util.* - - -class HomeFragment : Fragment() { - - private var _binding: FragmentHomeBinding? = null - private val binding get() = _binding!! - - private val wellnessViewModel: FinancialTipsViewModel by viewModel() - private val bonusViewModel: BonusViewModel by sharedViewModel() - private val channelsViewModel: ChannelsViewModel by sharedViewModel() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - AnalyticsUtil.logAnalyticsEvent(getString(R.string.visit_screen, getString(R.string.visit_home)), requireContext()) - _binding = FragmentHomeBinding.inflate(inflater, container, false) - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - bonusViewModel.fetchBonuses() - wellnessViewModel.getTips() - setupBanner() - - binding.airtime.setOnClickListener { navigateTo(getTransferDirection(HoverAction.AIRTIME)) } - binding.transfer.setOnClickListener { navigateTo(getTransferDirection(HoverAction.P2P)) } - binding.merchant.setOnClickListener { navigateTo(HomeFragmentDirections.actionNavigationHomeToMerchantFragment()) } - binding.paybill.setOnClickListener { navigateTo(HomeFragmentDirections.actionNavigationHomeToPaybillFragment()) } - binding.requestMoney.setOnClickListener { navigateTo(HomeFragmentDirections.actionNavigationHomeToNavigationRequest()) } - - NetworkMonitor.StateLiveData.get().observe(viewLifecycleOwner) { - updateOfflineIndicator(it) - } - - setUpWellnessTips() - setKeVisibility() - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - channelsViewModel.accountEventFlow.collect { - navigateTo(getTransferDirection(HoverAction.AIRTIME, bonusViewModel.bonusList.value.bonuses.first().userChannel.toString())) - } - } - } - } - - private fun getTransferDirection(type: String, channelId: String? = null): NavDirections { - return HomeFragmentDirections.actionNavigationHomeToNavigationTransfer(type).also { - if (channelId != null) - it.channelId = channelId - } - } - - private fun setupBanner() { - bonusViewModel.getBonusList() - - collectLatestLifecycleFlow(bonusViewModel.bonusList) { bonusList -> - if (bonusList.bonuses.isNotEmpty()) { - with(binding.bonusCard) { - message.text = bonusList.bonuses.first().message - learnMore.movementMethod = LinkMovementMethod.getInstance() - } - binding.bonusCard.apply { - cardBonus.visibility = View.VISIBLE - cta.setOnClickListener { - AnalyticsUtil.logAnalyticsEvent(getString(R.string.clicked_bonus_airtime_banner), requireActivity()) - validateAccounts(bonusList.bonuses.first()) - } - } - } else binding.bonusCard.cardBonus.visibility = View.GONE - } - } - - private fun setKeVisibility() { - channelsViewModel.simCountryList.observe(viewLifecycleOwner) { - binding.merchant.visibility = if (showMpesaActions(it)) View.VISIBLE else View.GONE - binding.paybill.visibility = if (showMpesaActions(it)) View.VISIBLE else View.GONE - } - } - - private fun showMpesaActions(countryIsos: List): Boolean = countryIsos.any { it.contentEquals("KE", ignoreCase = true) } - - private fun navigateTo(navDirections: NavDirections) = (requireActivity() as MainActivity).checkPermissionsAndNavigate(navDirections) - - private fun updateOfflineIndicator(isConnected: Boolean) { - binding.offlineBadge.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_internet_off, 0, 0, 0) - binding.offlineBadge.visibility = if (isConnected) View.GONE else View.VISIBLE - } - - private fun setUpWellnessTips() = collectLatestLifecycleFlow(wellnessViewModel.tipState) { - if (it.tips.isNotEmpty()) - showTip(it.tips.first()) - else - binding.wellnessCard.tipsCard.visibility = View.GONE - } - - private fun showTip(tip: FinancialTip) { - tip.date?.let { - if (android.text.format.DateUtils.isToday(it)) { - with(binding.wellnessCard) { - tipsCard.visibility = View.VISIBLE - - title.text = tip.title - snippet.text = tip.snippet - - contentLayout.setOnClickListener { - NavUtil.navigate(findNavController(), HomeFragmentDirections.actionNavigationHomeToWellnessFragment(tip.id)) - } - - readMoreLayout.setOnClickListener { - NavUtil.navigate(findNavController(), HomeFragmentDirections.actionNavigationHomeToWellnessFragment(null)) - } - } - } else - Timber.i("No tips available today") - } - } - - private fun validateAccounts(bonus: Bonus) { - channelsViewModel.validateAccounts(bonus.userChannel) - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/home/MainActivity.kt b/app/src/main/java/com/hover/stax/home/MainActivity.kt index 05b5848dc..91aef6355 100644 --- a/app/src/main/java/com/hover/stax/home/MainActivity.kt +++ b/app/src/main/java/com/hover/stax/home/MainActivity.kt @@ -11,7 +11,7 @@ import com.hover.stax.MainNavigationDirections import com.hover.stax.R import com.hover.stax.bonus.BonusViewModel import com.hover.stax.databinding.ActivityMainBinding -import com.hover.stax.financialTips.FinancialTipsFragment +import com.hover.stax.presentation.financial_tips.FinancialTipsFragment import com.hover.stax.login.AbstractGoogleAuthActivity import com.hover.stax.notifications.PushNotificationTopicsInterface import com.hover.stax.requests.NewRequestViewModel @@ -117,7 +117,7 @@ class MainActivity : AbstractGoogleAuthActivity(), BiometricChecker.AuthListener sendSms(requestViewModel, this) } else if (requestCode == SMS) { AnalyticsUtil.logAnalyticsEvent(getString(R.string.perms_sms_denied), this) - UIHelper.flashMessage(this, getString(R.string.toast_error_smsperm)) + UIHelper.flashAndReportMessage(this, getString(R.string.toast_error_smsperm)) } } diff --git a/app/src/main/java/com/hover/stax/home/NavHelper.kt b/app/src/main/java/com/hover/stax/home/NavHelper.kt index 84f89d8d2..48d583583 100644 --- a/app/src/main/java/com/hover/stax/home/NavHelper.kt +++ b/app/src/main/java/com/hover/stax/home/NavHelper.kt @@ -42,7 +42,7 @@ class NavHelper(val activity: AppCompatActivity) { navController?.let { NavigationUI.setupWithNavController(nav, navController!!) appBarConfiguration = AppBarConfiguration.Builder( - R.id.navigation_home, R.id.navigation_balance, R.id.navigation_history, R.id.libraryFragment, R.id.navigation_settings + R.id.navigation_home, R.id.navigation_history, R.id.libraryFragment ).build() } @@ -50,8 +50,6 @@ class NavHelper(val activity: AppCompatActivity) { setDestinationChangeListener(nav) } - fun showTxnDetails(uuid: String, isNewTransaction: Boolean? = false) = navController?.let { NavUtil.showTransactionDetailsFragment(it, uuid, isNewTransaction!!) } - fun navigateWellness(tipId: String?) = navController?.let { NavUtil.navigate(it, MainNavigationDirections.actionGlobalWellnessFragment(tipId)) } @@ -79,9 +77,6 @@ class NavHelper(val activity: AppCompatActivity) { private fun setDestinationChangeListener(nav: BottomNavigationView) = navController?.let { it.addOnDestinationChangedListener { _, destination, _ -> nav.visibility = if (destination.id == R.id.navigation_linkAccount) View.GONE else View.VISIBLE - - if (destination.id == R.id.bountyEmailFragment || destination.id == R.id.bountyListFragment) - nav.menu.findItem(R.id.navigation_settings).isChecked = true } } @@ -111,7 +106,6 @@ class NavHelper(val activity: AppCompatActivity) { R.id.navigation_settings, NAV_SETTINGS -> MainNavigationDirections.actionGlobalNavigationSettings() R.id.navigation_home, NAV_HOME -> MainNavigationDirections.actionGlobalNavigationHome() R.id.libraryFragment, NAV_USSD_LIB -> MainNavigationDirections.actionGlobalLibraryFragment() - R.id.navigation_balance, NAV_BALANCE -> MainNavigationDirections.actionGlobalNavigationBalance() NAV_TRANSFER -> MainNavigationDirections.actionGlobalTransferFragment(HoverAction.P2P) NAV_AIRTIME -> MainNavigationDirections.actionGlobalTransferFragment(HoverAction.AIRTIME) NAV_LINK_ACCOUNT -> MainNavigationDirections.actionGlobalAddChannelsFragment() diff --git a/app/src/main/java/com/hover/stax/hover/AbstractHoverCallerActivity.kt b/app/src/main/java/com/hover/stax/hover/AbstractHoverCallerActivity.kt index d38e99c42..14ab6c711 100644 --- a/app/src/main/java/com/hover/stax/hover/AbstractHoverCallerActivity.kt +++ b/app/src/main/java/com/hover/stax/hover/AbstractHoverCallerActivity.kt @@ -7,8 +7,8 @@ import com.hover.sdk.actions.HoverAction import com.hover.sdk.api.HoverParameters import com.hover.sdk.transactions.TransactionContract import com.hover.stax.R -import com.hover.stax.accounts.Account -import com.hover.stax.balances.BalancesViewModel +import com.hover.stax.domain.model.Account +import com.hover.stax.presentation.home.BalancesViewModel import com.hover.stax.home.NavHelper import com.hover.stax.notifications.PushNotificationTopicsInterface import com.hover.stax.schedules.Schedule @@ -41,7 +41,7 @@ abstract class AbstractHoverCallerActivity : AppCompatActivity(), PushNotificati hsb.run() updatePushNotifGroupStatus() } catch (e: Exception) { - runOnUiThread { UIHelper.flashMessage(this, getString(R.string.error_running_action)) } + runOnUiThread { UIHelper.flashAndReportMessage(this, getString(R.string.error_running_action)) } createLog(hsb, "Failed Actions") } @@ -93,7 +93,6 @@ abstract class AbstractHoverCallerActivity : AppCompatActivity(), PushNotificati BOUNTY_REQUEST -> showBountyDetails(data) FEE_REQUEST -> showFeeDetails(data) else -> { - balancesViewModel.setBalanceState(true) navToTransactionDetail(data) } } @@ -105,7 +104,7 @@ abstract class AbstractHoverCallerActivity : AppCompatActivity(), PushNotificati else "" } - private fun showMessage(str: String) = UIHelper.flashMessage(this, findViewById(R.id.fab), str) + private fun showMessage(str: String) = UIHelper.showAndReportSnackBar(this, findViewById(R.id.fab), str) private fun showBountyDetails(data: Intent?) { Timber.i("Request code is bounty") diff --git a/app/src/main/java/com/hover/stax/hover/HoverSession.kt b/app/src/main/java/com/hover/stax/hover/HoverSession.kt index b8529eed2..dc945d474 100644 --- a/app/src/main/java/com/hover/stax/hover/HoverSession.kt +++ b/app/src/main/java/com/hover/stax/hover/HoverSession.kt @@ -7,9 +7,9 @@ import com.hover.sdk.actions.HoverAction import com.hover.sdk.api.Hover import com.hover.sdk.api.HoverParameters import com.hover.stax.R -import com.hover.stax.accounts.ACCOUNT_ID -import com.hover.stax.accounts.ACCOUNT_NAME -import com.hover.stax.accounts.Account +import com.hover.stax.domain.model.ACCOUNT_ID +import com.hover.stax.domain.model.ACCOUNT_NAME +import com.hover.stax.domain.model.Account import com.hover.stax.contacts.PhoneHelper import com.hover.stax.settings.TEST_MODE import com.hover.stax.utils.AnalyticsUtil @@ -113,7 +113,6 @@ class HoverSession private constructor(b: Builder) { init { requireNotNull(a) { "Action must not be null" } - requireNotNull(c) { "Account must not be null" } this.activity = activity account = c action = a diff --git a/app/src/main/java/com/hover/stax/hover/TransactionReceiver.kt b/app/src/main/java/com/hover/stax/hover/TransactionReceiver.kt index bba16efb0..0d0f1214a 100644 --- a/app/src/main/java/com/hover/stax/hover/TransactionReceiver.kt +++ b/app/src/main/java/com/hover/stax/hover/TransactionReceiver.kt @@ -5,15 +5,15 @@ import android.content.Context import android.content.Intent import com.hover.sdk.actions.HoverAction import com.hover.sdk.transactions.TransactionContract -import com.hover.stax.accounts.ACCOUNT_ID -import com.hover.stax.accounts.Account -import com.hover.stax.accounts.AccountRepo -import com.hover.stax.accounts.PLACEHOLDER -import com.hover.stax.actions.ActionRepo +import com.hover.stax.domain.model.ACCOUNT_ID +import com.hover.stax.domain.model.Account +import com.hover.stax.data.local.accounts.AccountRepo +import com.hover.stax.data.local.actions.ActionRepo import com.hover.stax.channels.Channel -import com.hover.stax.channels.ChannelRepo +import com.hover.stax.data.local.channels.ChannelRepo import com.hover.stax.contacts.ContactRepo import com.hover.stax.contacts.StaxContact +import com.hover.stax.domain.model.PLACEHOLDER import com.hover.stax.merchants.MerchantRepo import com.hover.stax.paybill.BUSINESS_NAME import com.hover.stax.paybill.BUSINESS_NO @@ -196,7 +196,7 @@ class TransactionReceiver : BroadcastReceiver(), KoinComponent { parsedAccounts.forEach { if (savedAccounts.contains(it.channelId)) { Timber.e("Removing ${it.channelId} from ${it.name}") - accountRepo.deleteAccount(it.channelId, PLACEHOLDER) + accountRepo.deleteAccount(it.channelId, it.name.plus(PLACEHOLDER)) } } } diff --git a/app/src/main/java/com/hover/stax/inapp_banner/BannerUtils.kt b/app/src/main/java/com/hover/stax/inapp_banner/BannerUtils.kt index 3e429ae22..01fed00a6 100644 --- a/app/src/main/java/com/hover/stax/inapp_banner/BannerUtils.kt +++ b/app/src/main/java/com/hover/stax/inapp_banner/BannerUtils.kt @@ -2,8 +2,7 @@ package com.hover.stax.inapp_banner import android.content.Context import com.hover.sdk.permissions.PermissionHelper -import com.hover.stax.accounts.AccountRepo -import com.hover.stax.schedules.ScheduleRepo +import com.hover.stax.data.local.accounts.AccountRepo import com.hover.stax.utils.DateUtils import com.hover.stax.utils.Utils import kotlinx.coroutines.* diff --git a/app/src/main/java/com/hover/stax/login/AbstractGoogleAuthActivity.kt b/app/src/main/java/com/hover/stax/login/AbstractGoogleAuthActivity.kt index 29fb1525a..4dfde6a16 100644 --- a/app/src/main/java/com/hover/stax/login/AbstractGoogleAuthActivity.kt +++ b/app/src/main/java/com/hover/stax/login/AbstractGoogleAuthActivity.kt @@ -15,8 +15,8 @@ import com.google.android.play.core.install.model.InstallStatus import com.google.android.play.core.install.model.UpdateAvailability import com.hover.stax.BuildConfig import com.hover.stax.R -import com.hover.stax.bounties.BountyEmailFragmentDirections import com.hover.stax.hover.AbstractHoverCallerActivity +import com.hover.stax.presentation.bounties.BountyEmailFragmentDirections import com.hover.stax.settings.SettingsFragment import com.hover.stax.utils.UIHelper import org.koin.androidx.viewmodel.ext.android.viewModel @@ -74,7 +74,7 @@ abstract class AbstractGoogleAuthActivity : AbstractHoverCallerActivity(), StaxG it?.let { staxGoogleLoginInterface.googleLoginFailed() } } - user.observe(this@AbstractGoogleAuthActivity) { + googleUser.observe(this@AbstractGoogleAuthActivity) { it?.let { staxGoogleLoginInterface.googleLoginSuccessful() } } } @@ -137,7 +137,7 @@ abstract class AbstractGoogleAuthActivity : AbstractHoverCallerActivity(), StaxG } override fun googleLoginFailed() { - UIHelper.flashMessage(this, R.string.login_google_err) + UIHelper.flashAndReportMessage(this, R.string.login_google_err) } companion object { diff --git a/app/src/main/java/com/hover/stax/login/LoginViewModel.kt b/app/src/main/java/com/hover/stax/login/LoginViewModel.kt index a878eae32..d52f08721 100644 --- a/app/src/main/java/com/hover/stax/login/LoginViewModel.kt +++ b/app/src/main/java/com/hover/stax/login/LoginViewModel.kt @@ -3,7 +3,9 @@ package com.hover.stax.login import android.app.Application import android.content.Context import android.content.Intent -import androidx.lifecycle.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInClient @@ -14,7 +16,6 @@ import com.hover.stax.user.StaxUser import com.hover.stax.user.UserRepo import com.hover.stax.utils.AnalyticsUtil import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.json.JSONObject import timber.log.Timber @@ -24,8 +25,9 @@ class LoginViewModel(application: Application, private val userRepo: UserRepo, p lateinit var signInClient: GoogleSignInClient - val user = MutableLiveData() - val staxUser = MutableLiveData() + val googleUser = MutableLiveData() + var staxUser = MutableLiveData() + private set var progress = MutableLiveData(-1) var error = MutableLiveData() @@ -127,7 +129,7 @@ class LoginViewModel(application: Application, private val userRepo: UserRepo, p private fun setUser(signInAccount: GoogleSignInAccount, idToken: String) { Timber.e("setting user: %s", signInAccount.email) - user.postValue(signInAccount) + googleUser.postValue(signInAccount) progress.value = 33 uploadUserToStax(signInAccount.email, signInAccount.displayName, idToken) diff --git a/app/src/main/java/com/hover/stax/merchants/Merchant.kt b/app/src/main/java/com/hover/stax/merchants/Merchant.kt index 6ecb117d2..839a7733b 100644 --- a/app/src/main/java/com/hover/stax/merchants/Merchant.kt +++ b/app/src/main/java/com/hover/stax/merchants/Merchant.kt @@ -1,8 +1,8 @@ package com.hover.stax.merchants import androidx.room.* -import com.hover.stax.accounts.Account import com.hover.stax.channels.Channel +import com.hover.stax.domain.model.Account import javax.annotation.Nullable @Entity(tableName = "merchants", @@ -56,6 +56,6 @@ data class Merchant( } fun hasName(): Boolean { - return businessName != null && !businessName!!.isEmpty() + return businessName != null && businessName!!.isNotEmpty() } } \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/merchants/MerchantArrayAdapter.kt b/app/src/main/java/com/hover/stax/merchants/MerchantArrayAdapter.kt index 4cc3d7728..ed8d44cbe 100644 --- a/app/src/main/java/com/hover/stax/merchants/MerchantArrayAdapter.kt +++ b/app/src/main/java/com/hover/stax/merchants/MerchantArrayAdapter.kt @@ -8,6 +8,8 @@ import android.widget.ArrayAdapter import android.widget.Filter import android.widget.TextView import com.hover.stax.databinding.StaxSpinner2lineBinding +import java.util.* +import kotlin.collections.ArrayList class MerchantArrayAdapter(context: Context, val allMerchants: List): ArrayAdapter(context, 0, allMerchants) { @@ -39,8 +41,8 @@ class MerchantArrayAdapter(context: Context, val allMerchants: List): val filtered: MutableList = ArrayList() if (constraint != null) { for (merchant in allMerchants) { - if (merchant.toString().replace(" ".toRegex(), "").lowercase() - .contains(constraint.toString().lowercase()) + if (merchant.toString().replace(" ".toRegex(), "").lowercase(Locale.getDefault()) + .contains(constraint.toString().lowercase(Locale.getDefault())) ) { filtered.add(merchant) } diff --git a/app/src/main/java/com/hover/stax/merchants/MerchantFragment.kt b/app/src/main/java/com/hover/stax/merchants/MerchantFragment.kt index b83a7cc69..49db6a946 100644 --- a/app/src/main/java/com/hover/stax/merchants/MerchantFragment.kt +++ b/app/src/main/java/com/hover/stax/merchants/MerchantFragment.kt @@ -14,10 +14,9 @@ import com.hover.stax.contacts.StaxContact import com.hover.stax.databinding.FragmentMerchantBinding import com.hover.stax.hover.AbstractHoverCallerActivity import com.hover.stax.transfers.AbstractFormFragment -import com.hover.stax.transfers.TransferFragmentDirections import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.Utils -import com.hover.stax.utils.collectLatestLifecycleFlow +import com.hover.stax.utils.collectLifecycleFlow import com.hover.stax.views.AbstractStatefulInput import org.koin.androidx.viewmodel.ext.android.getSharedViewModel import timber.log.Timber @@ -52,7 +51,6 @@ class MerchantFragment : AbstractFormFragment() { override fun startObservers(root: View) { super.startObservers(root) - observeAccountList() observeActiveAccount() observeActions() observeActionSelection() @@ -61,13 +59,6 @@ class MerchantFragment : AbstractFormFragment() { observeRecentMerchants() } - private fun observeAccountList() { - collectLatestLifecycleFlow(accountsViewModel.accounts) { - if (it.isEmpty()) - setDropdownTouchListener(MerchantFragmentDirections.actionMerchantFragmentToAccountsFragment()) - } - } - private fun observeActiveAccount() { accountsViewModel.activeAccount.observe(viewLifecycleOwner) { account -> account?.let { binding.summaryCard.accountValue.setTitle(it.toString()) } diff --git a/app/src/main/java/com/hover/stax/merchants/MerchantViewModel.kt b/app/src/main/java/com/hover/stax/merchants/MerchantViewModel.kt index b8649e8e2..7443f25cd 100644 --- a/app/src/main/java/com/hover/stax/merchants/MerchantViewModel.kt +++ b/app/src/main/java/com/hover/stax/merchants/MerchantViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.hover.sdk.actions.HoverAction import com.hover.stax.R -import com.hover.stax.accounts.Account +import com.hover.stax.domain.model.Account import com.hover.stax.contacts.ContactRepo import com.hover.stax.paybill.BUSINESS_NO import com.hover.stax.schedules.ScheduleRepo @@ -15,7 +15,7 @@ import com.hover.stax.utils.DateUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class MerchantViewModel(application: Application, contactRepo: ContactRepo, val merchantRepo: MerchantRepo, scheduleRepo: ScheduleRepo) : AbstractFormViewModel(application, contactRepo, scheduleRepo) { +class MerchantViewModel(application: Application, contactRepo: ContactRepo, private val merchantRepo: MerchantRepo, scheduleRepo: ScheduleRepo) : AbstractFormViewModel(application, contactRepo, scheduleRepo) { val amount = MutableLiveData() val merchant = MutableLiveData() diff --git a/app/src/main/java/com/hover/stax/notifications/MessagingService.kt b/app/src/main/java/com/hover/stax/notifications/MessagingService.kt index 3d475e2d8..a8b143ef5 100644 --- a/app/src/main/java/com/hover/stax/notifications/MessagingService.kt +++ b/app/src/main/java/com/hover/stax/notifications/MessagingService.kt @@ -16,7 +16,7 @@ import com.google.firebase.messaging.RemoteMessage import com.hover.stax.FRAGMENT_DIRECT import com.hover.stax.FROM_FCM import com.hover.stax.R -import com.hover.stax.financialTips.FinancialTipsFragment +import com.hover.stax.presentation.financial_tips.FinancialTipsFragment import com.hover.stax.home.MainActivity import timber.log.Timber import kotlin.random.Random diff --git a/app/src/main/java/com/hover/stax/onboarding/OnBoardingActivity.kt b/app/src/main/java/com/hover/stax/onboarding/OnBoardingActivity.kt index bc9201efc..4b1c5c447 100644 --- a/app/src/main/java/com/hover/stax/onboarding/OnBoardingActivity.kt +++ b/app/src/main/java/com/hover/stax/onboarding/OnBoardingActivity.kt @@ -8,19 +8,18 @@ import com.hover.sdk.permissions.PermissionHelper import com.hover.stax.FRAGMENT_DIRECT import com.hover.stax.OnboardingNavigationDirections import com.hover.stax.R -import com.hover.stax.VARIANT import com.hover.stax.databinding.OnboardingLayoutBinding import com.hover.stax.home.MainActivity import com.hover.stax.home.NAV_HOME import com.hover.stax.home.NAV_LINK_ACCOUNT import com.hover.stax.login.AbstractGoogleAuthActivity -import com.hover.stax.login.StaxGoogleLoginInterface -import com.hover.stax.onboarding.signInVariant.SignInVariantFragmentDirections import com.hover.stax.permissions.PermissionUtils -import com.hover.stax.utils.* -import timber.log.Timber +import com.hover.stax.utils.AnalyticsUtil +import com.hover.stax.utils.NavUtil +import com.hover.stax.utils.UIHelper +import com.hover.stax.utils.Utils -class OnBoardingActivity : AbstractGoogleAuthActivity(), StaxGoogleLoginInterface { +class OnBoardingActivity : AbstractGoogleAuthActivity() { private lateinit var binding: OnboardingLayoutBinding private lateinit var navController: NavController @@ -35,7 +34,6 @@ class OnBoardingActivity : AbstractGoogleAuthActivity(), StaxGoogleLoginInterfac setupNavigation() navigateNextScreen() - setGoogleLoginInterface(this) } private fun setupNavigation() { @@ -50,14 +48,6 @@ class OnBoardingActivity : AbstractGoogleAuthActivity(), StaxGoogleLoginInterfac else NavUtil.navigate(navController, OnboardingNavigationDirections.actionGlobalInteractiveOnboardingVariant()) } - override fun googleLoginSuccessful() { - NavUtil.navigate(navController, SignInVariantFragmentDirections.actionSignInVariantFragmentToWelcomeFragment(1)) - } - - override fun googleLoginFailed() { - UIHelper.flashMessage(this, R.string.login_google_err) - } - fun checkPermissionsAndNavigate() { val permissionHelper = PermissionHelper(this) if (permissionHelper.hasBasicPerms()) navigateToMainActivity() diff --git a/app/src/main/java/com/hover/stax/onboarding/defaultVariant/DefaultVariantFragment.kt b/app/src/main/java/com/hover/stax/onboarding/defaultVariant/DefaultVariantFragment.kt deleted file mode 100644 index eb8a19bae..000000000 --- a/app/src/main/java/com/hover/stax/onboarding/defaultVariant/DefaultVariantFragment.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.hover.stax.onboarding.defaultVariant - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import com.hover.stax.R -import com.hover.stax.databinding.FragmentDefaultVariantBinding -import com.hover.stax.onboarding.OnBoardingActivity -import com.hover.stax.utils.AnalyticsUtil - -class DefaultVariantFragment : Fragment() { - - private var _binding: FragmentDefaultVariantBinding? = null - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentDefaultVariantBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - AnalyticsUtil.logAnalyticsEvent(getString(R.string.visit_screen, getString(R.string.visit_onboarding)), requireActivity()) - initContinueButton() - } - - private fun initContinueButton() = binding.onboardingContinueBtn.setOnClickListener { - AnalyticsUtil.logAnalyticsEvent(getString(R.string.clicked_getstarted), requireContext()) - (requireActivity() as OnBoardingActivity).checkPermissionsAndNavigate() - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/onboarding/signInVariant/SignInVariantFragment.kt b/app/src/main/java/com/hover/stax/onboarding/signInVariant/SignInVariantFragment.kt deleted file mode 100644 index 4ecdfc8c8..000000000 --- a/app/src/main/java/com/hover/stax/onboarding/signInVariant/SignInVariantFragment.kt +++ /dev/null @@ -1,214 +0,0 @@ -package com.hover.stax.onboarding.signInVariant - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.ValueAnimator -import android.os.Bundle -import android.text.method.LinkMovementMethod -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback -import androidx.core.content.ContextCompat -import androidx.core.text.HtmlCompat -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController -import androidx.viewpager.widget.ViewPager -import com.google.android.material.progressindicator.LinearProgressIndicator -import com.hover.stax.R -import com.hover.stax.databinding.FragmentSigninVariantBinding -import com.hover.stax.onboarding.OnBoardingActivity -import com.hover.stax.utils.AnalyticsUtil -import com.hover.stax.utils.NavUtil -import timber.log.Timber - - -class SignInVariantFragment : Fragment(), ViewPager.OnPageChangeListener { - - private var _binding: FragmentSigninVariantBinding? = null - private val binding get() = _binding!! - - private lateinit var progressBar1: LinearProgressIndicator - private lateinit var progressBar2: LinearProgressIndicator - private lateinit var progressBar3: LinearProgressIndicator - private lateinit var progressBar4: LinearProgressIndicator - - private lateinit var animator1: ValueAnimator - private lateinit var animator2: ValueAnimator - private lateinit var animator3: ValueAnimator - private lateinit var animator4: ValueAnimator - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = FragmentSigninVariantBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - AnalyticsUtil.logAnalyticsEvent(getString(R.string.visit_screen, getString(R.string.visit_sign_in)), requireActivity()) - - initProgressBarView() - initAnimators() - - setUpSlides() - - setupPrivacyPolicy() - setupTermsOfService() - - setupSignInWithGoogle() - setupContinueNoSignIn() - - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedCallback) - } - - private fun setupSignInWithGoogle() = binding.continueWithGoogle.setOnClickListener { - AnalyticsUtil.logAnalyticsEvent(getString(R.string.clicked_google_sign_in), requireActivity()) - (requireActivity() as OnBoardingActivity).signIn() - } - - private fun setupContinueNoSignIn() = binding.continueNoSignIn.setOnClickListener { - AnalyticsUtil.logAnalyticsEvent(getString(R.string.clicked_skip_sign_in), requireActivity()) - NavUtil.navigate(findNavController(), SignInVariantFragmentDirections.actionSignInVariantFragmentToWelcomeFragment(1)) - } - - private fun initProgressBarView() { - progressBar1 = binding.pb1 - progressBar2 = binding.pb2 - progressBar3 = binding.pb3 - progressBar4 = binding.pb4 - - val brightBlue = ContextCompat.getColor(requireActivity(), R.color.brightBlue) - progressBar1.trackColor = brightBlue - progressBar2.trackColor = brightBlue - progressBar3.trackColor = brightBlue - progressBar4.trackColor = brightBlue - - val deepBlue = ContextCompat.getColor(requireActivity(), R.color.stax_state_blue) - progressBar1.setIndicatorColor(deepBlue) - progressBar2.setIndicatorColor(deepBlue) - progressBar3.setIndicatorColor(deepBlue) - progressBar4.setIndicatorColor(deepBlue) - } - - private fun initAnimators() { - animator1 = ValueAnimator.ofInt(0, progressBar1.max) - animator2 = ValueAnimator.ofInt(0, progressBar2.max) - animator3 = ValueAnimator.ofInt(0, progressBar3.max) - animator4 = ValueAnimator.ofInt(0, progressBar4.max) - } - - private fun setUpSlides() { - val viewPagerAdapter = SlidesPagerAdapter(requireContext()) - binding.vpPager.apply { - startAutoScroll(FIRST_SCROLL_DELAY) - setInterval(SCROLL_INTERVAL) - setCycle(true) - setAutoScrollDurationFactor(AUTO_SCROLL_EASE_DURATION_FACTOR) - setSwipeScrollDurationFactor(SWIPE_DURATION_FACTOR) - setStopScrollWhenTouch(true) - addOnPageChangeListener(this@SignInVariantFragment) - adapter = viewPagerAdapter - } - } - - private fun setupPrivacyPolicy() { - binding.onboardingV1Tos.text = HtmlCompat.fromHtml(requireContext().getString(R.string.privacyPolicyFullLabel), HtmlCompat.FROM_HTML_MODE_LEGACY) - binding.onboardingV1Tos.movementMethod = LinkMovementMethod.getInstance() - } - - private fun setupTermsOfService() { - binding.onboardingV1PrivacyPolicy.text = HtmlCompat.fromHtml(requireContext().getString(R.string.termsOfServiceFullLabel), HtmlCompat.FROM_HTML_MODE_LEGACY) - binding.onboardingV1PrivacyPolicy.movementMethod = LinkMovementMethod.getInstance() - } - - private fun updateProgressAnimation(animator: ValueAnimator, progressBar: LinearProgressIndicator) = animator.apply { - duration = 3500 - addUpdateListener { animation -> - progressBar.progress = animation.animatedValue as Int - if (progressBar.progress > 90) { - fillUpProgress(progressBar) - } - } - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - super.onAnimationEnd(animation) - } - }) - start() - } - - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { - showProgress(position) - } - - override fun onPageSelected(position: Int) {} - - override fun onPageScrollStateChanged(state: Int) {} - - private fun showProgress(currentPos: Int) { - when (currentPos) { - 0 -> { - updateProgressAnimation(animator1, progressBar1) - resetFilledProgress(animator2, progressBar2) - resetFilledProgress(animator3, progressBar3) - resetFilledProgress(animator4, progressBar4) - } - - 1 -> { - fillUpProgress(progressBar1) - updateProgressAnimation(animator2, progressBar2) - resetFilledProgress(animator3, progressBar3) - resetFilledProgress(animator4, progressBar4) - } - - 2 -> { - fillUpProgress(progressBar1) - fillUpProgress(progressBar2) - updateProgressAnimation(animator3, progressBar3) - resetFilledProgress(animator4, progressBar4) - } - - 3 -> { - fillUpProgress(progressBar1) - fillUpProgress(progressBar2) - fillUpProgress(progressBar3) - updateProgressAnimation(animator4, progressBar4) - } - } - } - - private fun fillUpProgress(progressBar: LinearProgressIndicator) { - try { - val deepBlue = ContextCompat.getColor(requireActivity(), R.color.stax_state_blue) - progressBar.progress = 100 - progressBar.trackColor = deepBlue - } catch (e: IllegalStateException) { - Timber.i("animation needed to complete") - } - } - - private fun resetFilledProgress(animator: ValueAnimator, progressBar: LinearProgressIndicator) { - animator.cancel() - val brightBlue = ContextCompat.getColor(requireActivity(), R.color.brightBlue) - progressBar.progress = 0 - progressBar.trackColor = brightBlue - } - - private val backPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - Timber.i("Back navigation disabled") //do nothing to prevent navigation back to the home fragment (default variant) - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - companion object { - const val FIRST_SCROLL_DELAY = 3500 - const val SCROLL_INTERVAL = 3500L - const val SWIPE_DURATION_FACTOR = 2.0 - const val AUTO_SCROLL_EASE_DURATION_FACTOR = 5.0 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/onboarding/signInVariant/SlidesPagerAdapter.kt b/app/src/main/java/com/hover/stax/onboarding/signInVariant/SlidesPagerAdapter.kt deleted file mode 100644 index bfc081e9c..000000000 --- a/app/src/main/java/com/hover/stax/onboarding/signInVariant/SlidesPagerAdapter.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.hover.stax.onboarding.signInVariant - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.viewpager.widget.PagerAdapter -import androidx.viewpager.widget.ViewPager -import com.hover.stax.R -import com.hover.stax.databinding.ItemSiginViewpagerBinding - -private data class SlideData(val imgRes: Int, val titleRes: Int, val descRes: Int) - -class SlidesPagerAdapter(private val context: Context) : PagerAdapter() { - - override fun destroyItem(container: ViewGroup, position: Int, arg1: Any) { - (container as ViewPager).removeView(arg1 as View?) - } - - override fun instantiateItem(container: ViewGroup, position: Int): Any { - val binding = ItemSiginViewpagerBinding.inflate(LayoutInflater.from(context), container, false) - - val slideData = getSlideData(position) - binding.onboardingV1Title.setText(slideData.titleRes) - binding.onboardingV1Desc.setText(slideData.descRes) - binding.onboardingV1Image.setImageResource(slideData.imgRes) - - container.addView(binding.root) - return binding.root - } - - override fun getCount(): Int { - return 4 - } - - override fun isViewFromObject(view: View, arg1: Any): Boolean { - return view == arg1 as View? - } - - private fun getSlideData(position: Int): SlideData { - return when (position) { - 0 -> SlideData(R.drawable.send_illustration, R.string.onboarding_v1_slide1_title, R.string.slide1_desc) - 1 -> SlideData(R.drawable.send_illustration, R.string.onboarding_v1_slide2_title, R.string.slide2_desc) - 2 -> SlideData(R.drawable.request_illustration, R.string.onboarding_v1_slide3_title, R.string.slide3_desc) - else -> SlideData(R.drawable.airtime_illustration, R.string.onboarding_v1_slide4_title, R.string.slide4_desc) - } - } - -} - diff --git a/app/src/main/java/com/hover/stax/onboarding/signInVariant/StaxAutoScrollViewPager.kt b/app/src/main/java/com/hover/stax/onboarding/signInVariant/StaxAutoScrollViewPager.kt deleted file mode 100644 index c631fc5d5..000000000 --- a/app/src/main/java/com/hover/stax/onboarding/signInVariant/StaxAutoScrollViewPager.kt +++ /dev/null @@ -1,200 +0,0 @@ -package com.hover.stax.onboarding.signInVariant - -import android.content.Context -import android.os.Handler -import android.os.Message -import android.util.AttributeSet -import android.view.MotionEvent -import android.view.animation.Interpolator -import android.widget.Scroller -import androidx.viewpager.widget.ViewPager -import java.lang.ref.WeakReference - -class StaxAutoScrollViewPager : ViewPager { - - private var interval = DEFAULT_INTERVAL.toLong() - private var direction = RIGHT - private var isCycle = true - private var stopScrollWhenTouch = true - private val slideBorderMode = SLIDE_BORDER_MODE_NONE - private val isBorderAnimation = true - private var autoScrollFactor = 1.0 - private var swipeScrollFactor = 1.0 - private lateinit var handler: MyHandler - private var isAutoScroll = false - private var isStopByTouch = false - private var touchX = 0f - private var downX = 0f - private var scroller: CustomDurationScroller? = null - - constructor(paramContext: Context?) : super(paramContext!!) { - init() - } - - constructor(paramContext: Context?, paramAttributeSet: AttributeSet?) : super(paramContext!!, - paramAttributeSet) { - init() - } - - private fun init() { - handler = MyHandler(this) - setViewPagerScroller() - } - - private fun startAutoScroll() { - isAutoScroll = true - sendScrollMessage((interval + scroller!!.duration / autoScrollFactor * swipeScrollFactor).toLong()) - } - - fun startAutoScroll(delayTimeInMills: Int) { - isAutoScroll = true - sendScrollMessage(delayTimeInMills.toLong()) - } - - private fun stopAutoScroll() { - isAutoScroll = false - handler.removeMessages(SCROLL_WHAT) - } - - fun setSwipeScrollDurationFactor(scrollFactor: Double) { - swipeScrollFactor = scrollFactor - } - - fun setAutoScrollDurationFactor(scrollFactor: Double) { - autoScrollFactor = scrollFactor - } - - private fun sendScrollMessage(delayTimeInMills: Long) { - handler.removeMessages(SCROLL_WHAT) - handler.sendEmptyMessageDelayed(SCROLL_WHAT, delayTimeInMills) - } - - private fun setViewPagerScroller() { - try { - val scrollerField = ViewPager::class.java.getDeclaredField("mScroller") - scrollerField.isAccessible = true - val interpolatorField = ViewPager::class.java.getDeclaredField("sInterpolator") - interpolatorField.isAccessible = true - scroller = CustomDurationScroller(context, interpolatorField[null] as Interpolator) - scrollerField[this] = scroller - } catch (e: Exception) { - e.printStackTrace() - } - } - - fun scrollOnce() { - val adapter = adapter - var currentItem = currentItem - var totalCount = 0 - if (adapter == null || adapter.count.also { totalCount = it } <= 1) { - return - } - val nextItem = if (direction == LEFT) --currentItem else ++currentItem - if (nextItem < 0) { - if (isCycle) { - setCurrentItem(totalCount - 1, isBorderAnimation) - } - } else if (nextItem == totalCount) { - if (isCycle) { - setCurrentItem(0, isBorderAnimation) - } - } else { - setCurrentItem(nextItem, true) - } - } - - private fun pauseScrolling(ev: MotionEvent) { - if (ev.actionMasked == MotionEvent.ACTION_DOWN && isAutoScroll) { - isStopByTouch = true - stopAutoScroll() - } else if (ev.action == MotionEvent.ACTION_UP && isStopByTouch) { - startAutoScroll() - } - } - - override fun dispatchTouchEvent(ev: MotionEvent): Boolean { - if (stopScrollWhenTouch) pauseScrolling(ev) - - if (slideBorderMode == SLIDE_BORDER_MODE_TO_PARENT || slideBorderMode == SLIDE_BORDER_MODE_CYCLE) { - touchX = ev.x - if (ev.action == MotionEvent.ACTION_DOWN) { - downX = touchX - } - val currentItem = currentItem - val adapter = adapter - val pageCount = adapter?.count ?: 0 - if (currentItem == 0 && downX <= touchX || currentItem == pageCount - 1 && downX >= touchX) { - if (slideBorderMode == SLIDE_BORDER_MODE_TO_PARENT) { - parent.requestDisallowInterceptTouchEvent(false) - } else { - if (pageCount > 1) { - setCurrentItem(pageCount - currentItem - 1, isBorderAnimation) - } - parent.requestDisallowInterceptTouchEvent(true) - } - return super.dispatchTouchEvent(ev) - } - } - parent.requestDisallowInterceptTouchEvent(true) - return super.dispatchTouchEvent(ev) - } - - private class MyHandler(staxAutoScrollViewPager: StaxAutoScrollViewPager) : Handler() { - private val autoScrollViewPager: WeakReference = WeakReference(staxAutoScrollViewPager) - override fun handleMessage(msg: Message) { - super.handleMessage(msg) - if (msg.what == SCROLL_WHAT) { - val pager = autoScrollViewPager.get() - if (pager != null) { - pager.scroller!!.setScrollDurationFactor(pager.autoScrollFactor) - pager.scrollOnce() - pager.scroller!!.setScrollDurationFactor(pager.swipeScrollFactor) - pager.sendScrollMessage(pager.interval + pager.scroller!!.duration) - } - } - } - - } - - fun setStopScrollWhenTouch(stopScrollWhenTouch: Boolean) { - this.stopScrollWhenTouch = stopScrollWhenTouch - } - - fun setCycle(isCycle: Boolean) { - this.isCycle = isCycle - } - - fun setDirection(direction: Int) { - this.direction = direction - } - - fun setInterval(interval: Long) { - this.interval = interval - } - - fun getDirection(): Int { - return if (direction == LEFT) LEFT else RIGHT - } - - private class CustomDurationScroller(context: Context?, interpolator: Interpolator?) : - Scroller(context, interpolator) { - private var scrollFactor = 1.0 - fun setScrollDurationFactor(scrollFactor: Double) { - this.scrollFactor = scrollFactor - } - - override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) { - super.startScroll(startX, startY, dx, dy, (duration * scrollFactor).toInt()) - } - } - - companion object { - const val DEFAULT_INTERVAL = 1500 - const val LEFT = 0 - const val RIGHT = 1 - const val SLIDE_BORDER_MODE_NONE = 0 - const val SLIDE_BORDER_MODE_CYCLE = 1 - const val SLIDE_BORDER_MODE_TO_PARENT = 2 - const val SCROLL_WHAT = 0 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/paybill/PayBill.kt b/app/src/main/java/com/hover/stax/paybill/PayBill.kt index 2bec40dcc..efbf47cb6 100644 --- a/app/src/main/java/com/hover/stax/paybill/PayBill.kt +++ b/app/src/main/java/com/hover/stax/paybill/PayBill.kt @@ -2,8 +2,9 @@ package com.hover.stax.paybill import androidx.room.* import com.hover.sdk.actions.HoverAction -import com.hover.stax.accounts.Account + import com.hover.stax.channels.Channel +import com.hover.stax.domain.model.Account import javax.annotation.Nullable const val BUSINESS_NO = "businessNo" diff --git a/app/src/main/java/com/hover/stax/paybill/PaybillFragment.kt b/app/src/main/java/com/hover/stax/paybill/PaybillFragment.kt index 68f3641bd..e8ff06c44 100644 --- a/app/src/main/java/com/hover/stax/paybill/PaybillFragment.kt +++ b/app/src/main/java/com/hover/stax/paybill/PaybillFragment.kt @@ -17,7 +17,6 @@ import com.hover.stax.transfers.AbstractFormFragment import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.UIHelper import com.hover.stax.utils.Utils -import com.hover.stax.utils.collectLatestLifecycleFlow import com.hover.stax.views.AbstractStatefulInput import com.hover.stax.views.StaxDialog import com.hover.stax.views.StaxTextInput @@ -120,7 +119,7 @@ class PaybillFragment : AbstractFormFragment(), PaybillIconsAdapter.IconSelectLi viewModel.selectedPaybill.value?.isSaved == true -> viewModel.setEditing(false) else -> { viewModel.savePaybill(accountsViewModel.activeAccount.value, actionSelectViewModel.activeAction.value) - UIHelper.flashMessage(requireActivity(), R.string.paybill_save_success) + UIHelper.flashAndReportMessage(requireActivity(), R.string.paybill_save_success) } } } @@ -143,11 +142,6 @@ class PaybillFragment : AbstractFormFragment(), PaybillIconsAdapter.IconSelectLi viewModel.getSavedPaybills(account.id) } } - - collectLatestLifecycleFlow(accountsViewModel.accounts) { - if(it.isEmpty()) - setDropdownTouchListener(PaybillFragmentDirections.actionGlobalAddChannelsFragment()) - } } private fun observeActions() { @@ -180,7 +174,7 @@ class PaybillFragment : AbstractFormFragment(), PaybillIconsAdapter.IconSelectLi } private fun updateBiz(name: String?, no: String?) { - binding.editCard.businessNoInput.setMutlipartText(name, no) + binding.editCard.businessNoInput.setMultipartText(name, no) binding.summaryCard.recipient.setContent(name, no) } @@ -295,7 +289,7 @@ class PaybillFragment : AbstractFormFragment(), PaybillIconsAdapter.IconSelectLi .setPosButton(R.string.btn_update) { _ -> if (activity != null) { viewModel.updatePaybill(it) - UIHelper.flashMessage(requireActivity(), R.string.paybill_update_success) + UIHelper.flashAndReportMessage(requireActivity(), R.string.paybill_update_success) viewModel.setEditing(false) } } diff --git a/app/src/main/java/com/hover/stax/paybill/PaybillListFragment.kt b/app/src/main/java/com/hover/stax/paybill/PaybillListFragment.kt index c1e6bf064..09f6b0781 100644 --- a/app/src/main/java/com/hover/stax/paybill/PaybillListFragment.kt +++ b/app/src/main/java/com/hover/stax/paybill/PaybillListFragment.kt @@ -108,7 +108,7 @@ class PaybillListFragment : Fragment(), PaybillAdapter.ClickListener, PaybillAct .setPosButton(R.string.btn_delete) { if (activity != null) { paybillViewModel.deletePaybill(paybill) - UIHelper.flashMessage(requireActivity(), R.string.paybill_delete_success) + UIHelper.flashAndReportMessage(requireActivity(), R.string.paybill_delete_success) } } dialog!!.showIt() diff --git a/app/src/main/java/com/hover/stax/paybill/PaybillViewModel.kt b/app/src/main/java/com/hover/stax/paybill/PaybillViewModel.kt index 1d139d81b..8551e38ae 100644 --- a/app/src/main/java/com/hover/stax/paybill/PaybillViewModel.kt +++ b/app/src/main/java/com/hover/stax/paybill/PaybillViewModel.kt @@ -5,15 +5,15 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.hover.sdk.actions.HoverAction import com.hover.stax.R -import com.hover.stax.accounts.Account -import com.hover.stax.accounts.AccountRepo -import com.hover.stax.actions.ActionRepo + +import com.hover.stax.data.local.accounts.AccountRepo +import com.hover.stax.data.local.actions.ActionRepo import com.hover.stax.contacts.ContactRepo +import com.hover.stax.domain.model.Account import com.hover.stax.schedules.ScheduleRepo import com.hover.stax.transfers.AbstractFormViewModel import com.hover.stax.utils.AnalyticsUtil import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.json.JSONObject import timber.log.Timber @@ -42,10 +42,6 @@ class PaybillViewModel( } fun selectPaybill(paybill: Paybill) { - Timber.e("selecting paybill by paybill: %s", paybill.businessNo) - Timber.e("current amount: %s", amount.value) - Timber.e("isSaved: %s", paybill.isSaved) - selectedPaybill.value = paybill businessName.value = paybill.businessName diff --git a/app/src/main/java/com/hover/stax/presentation/bounties/BountiesState.kt b/app/src/main/java/com/hover/stax/presentation/bounties/BountiesState.kt new file mode 100644 index 000000000..2904f372f --- /dev/null +++ b/app/src/main/java/com/hover/stax/presentation/bounties/BountiesState.kt @@ -0,0 +1,9 @@ +package com.hover.stax.presentation.bounties + +import com.hover.stax.domain.model.ChannelBounties + +data class BountiesState( + var loading: Boolean = false, + var error: String = "", + var bounties: List = emptyList() +) \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/presentation/bounties/BountiesViewModel.kt b/app/src/main/java/com/hover/stax/presentation/bounties/BountiesViewModel.kt new file mode 100644 index 000000000..64332f867 --- /dev/null +++ b/app/src/main/java/com/hover/stax/presentation/bounties/BountiesViewModel.kt @@ -0,0 +1,103 @@ +package com.hover.stax.presentation.bounties + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.hover.sdk.api.Hover +import com.hover.sdk.sims.SimInfo +import com.hover.stax.countries.CountryAdapter +import com.hover.stax.domain.model.Bounty +import com.hover.stax.domain.model.Resource +import com.hover.stax.domain.use_case.bounties.GetChannelBountiesUseCase +import com.hover.stax.domain.use_case.channels.GetPresentSimsUseCase +import com.hover.stax.utils.Utils.getPackage +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +class BountyViewModel(private val simsUseCase: GetPresentSimsUseCase, private val bountiesUseCase: GetChannelBountiesUseCase, val application: Application) : ViewModel() { + + private val _countryList = MutableStateFlow>(emptyList()) + val countryList = _countryList.asStateFlow() + + private val _sims = MutableStateFlow>(emptyList()) + val sims = _sims.asStateFlow() + + private val _bountiesState = MutableStateFlow(BountiesState()) + val bountiesState = _bountiesState.asStateFlow() + + private val _country = MutableStateFlow(CountryAdapter.CODE_ALL_COUNTRIES) + val country = _country.asStateFlow() + + private val onBountySelectEvent = Channel() + val bountySelectEvent = onBountySelectEvent.receiveAsFlow() + + private var simReceiver: BroadcastReceiver? = null + + init { + simReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + fetchSims() + } + } + + loadBountyData() + } + + private fun loadBountyData() { + loadSims() + loadCountryList() + loadBounties() + } + + private fun loadCountryList() = viewModelScope.launch { + bountiesUseCase.getChannelList().collect { codes -> + _countryList.update { codes } + } + } + + private fun loadSims() { + fetchSims() + + simReceiver?.let { + LocalBroadcastManager.getInstance(application) + .registerReceiver(it, IntentFilter(getPackage(application) + ".NEW_SIM_INFO_ACTION")) + } + Hover.updateSimInfo(application) + } + + private fun fetchSims() = viewModelScope.launch(Dispatchers.IO) { + _sims.update { simsUseCase.presentSims } + } + + fun loadBounties(countryCode: String = CountryAdapter.CODE_ALL_COUNTRIES) { + _country.value = countryCode + bountiesUseCase.getBounties(countryCode).onEach { result -> + when (result) { + is Resource.Loading -> _bountiesState.update { it.copy(loading = true) } + is Resource.Error -> _bountiesState.update { it.copy(loading = false, error = result.message!!) } + is Resource.Success -> _bountiesState.update { it.copy(loading = false, bounties = result.data!!) } + } + }.launchIn(viewModelScope) + } + + fun isSimPresent(bounty: Bounty): Boolean = simsUseCase.simPresent(bounty, sims.value) + + fun handleBountyEvent(bountySelectEvent: BountySelectEvent) = viewModelScope.launch { + onBountySelectEvent.send(bountySelectEvent) + } + + override fun onCleared() { + try { + simReceiver?.let { LocalBroadcastManager.getInstance(application).unregisterReceiver(it) } + } catch (ignored: Exception) { + } + super.onCleared() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/bounties/BountyEmailFragment.kt b/app/src/main/java/com/hover/stax/presentation/bounties/BountyEmailFragment.kt similarity index 99% rename from app/src/main/java/com/hover/stax/bounties/BountyEmailFragment.kt rename to app/src/main/java/com/hover/stax/presentation/bounties/BountyEmailFragment.kt index 8a3574d5c..9abac9f46 100644 --- a/app/src/main/java/com/hover/stax/bounties/BountyEmailFragment.kt +++ b/app/src/main/java/com/hover/stax/presentation/bounties/BountyEmailFragment.kt @@ -1,4 +1,4 @@ -package com.hover.stax.bounties +package com.hover.stax.presentation.bounties import android.os.Bundle import android.text.method.LinkMovementMethod diff --git a/app/src/main/java/com/hover/stax/bounties/BountyListFragment.kt b/app/src/main/java/com/hover/stax/presentation/bounties/BountyListFragment.kt similarity index 51% rename from app/src/main/java/com/hover/stax/bounties/BountyListFragment.kt rename to app/src/main/java/com/hover/stax/presentation/bounties/BountyListFragment.kt index 5e399d5fc..3ccd48b70 100644 --- a/app/src/main/java/com/hover/stax/bounties/BountyListFragment.kt +++ b/app/src/main/java/com/hover/stax/presentation/bounties/BountyListFragment.kt @@ -1,11 +1,11 @@ -package com.hover.stax.bounties +package com.hover.stax.presentation.bounties import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.work.ExistingPeriodicWorkPolicy @@ -13,38 +13,34 @@ import androidx.work.ExistingWorkPolicy import androidx.work.WorkManager import com.hover.sdk.actions.HoverAction import com.hover.sdk.api.Hover -import com.hover.sdk.sims.SimInfo import com.hover.stax.R -import com.hover.stax.channels.Channel import com.hover.stax.channels.UpdateChannelsWorker -import com.hover.stax.countries.CountryAdapter +import com.hover.stax.data.remote.workers.UpdateBountyTransactionsWorker import com.hover.stax.databinding.FragmentBountyListBinding +import com.hover.stax.domain.model.Bounty import com.hover.stax.hover.AbstractHoverCallerActivity -import com.hover.stax.transactions.StaxTransaction import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.NavUtil -import com.hover.stax.utils.UIHelper import com.hover.stax.utils.Utils +import com.hover.stax.utils.collectLifecycleFlow import com.hover.stax.utils.network.NetworkMonitor -import com.hover.stax.views.AbstractStatefulInput import com.hover.stax.views.StaxDialog import kotlinx.coroutines.launch -import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber -class BountyListFragment : Fragment(), BountyListItem.SelectListener, CountryAdapter.SelectListener { +class BountyListFragment : Fragment() { private lateinit var networkMonitor: NetworkMonitor - private val bountyViewModel: BountyViewModel by sharedViewModel() + private val bountiesViewModel: BountyViewModel by viewModel() + private var _binding: FragmentBountyListBinding? = null private val binding get() = _binding!! private var dialog: StaxDialog? = null - private val bountyAdapter = BountyAdapter(this) - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { AnalyticsUtil.logAnalyticsEvent(getString(R.string.visit_screen, getString(R.string.visit_bounty_list)), requireActivity()) @@ -56,14 +52,10 @@ class BountyListFragment : Fragment(), BountyListItem.SelectListener, CountryAda super.onViewCreated(view, savedInstanceState) networkMonitor = NetworkMonitor(requireActivity()) - initRecyclerView() - - startObservers() + initBountyList() + observeBountyEvents() - binding.bountiesRecyclerView.adapter = bountyAdapter - binding.bountyCountryDropdown.isEnabled = false binding.countryFilter.apply { - showProgressIndicator() setOnClickIcon { NavUtil.navigate(findNavController(), BountyListFragmentDirections.actionBountyListFragmentToNavigationSettings()) } @@ -75,6 +67,13 @@ class BountyListFragment : Fragment(), BountyListItem.SelectListener, CountryAda forceUserToBeOnline() } + private fun observeBountyEvents() = collectLifecycleFlow(bountiesViewModel.bountySelectEvent) { + when (it) { + is BountySelectEvent.ViewBountyDetail -> viewBountyDetail(it.bounty) + is BountySelectEvent.ViewTransactionDetail -> NavUtil.showTransactionDetailsFragment(findNavController(), it.uuid) + } + } + private fun forceUserToBeOnline() { if (isAdded && networkMonitor.isNetworkConnected) { updateActionConfig() @@ -83,15 +82,17 @@ class BountyListFragment : Fragment(), BountyListItem.SelectListener, CountryAda } else showOfflineDialog() } - private fun updateActionConfig() = Hover.initialize(requireActivity(), object : Hover.DownloadListener { - override fun onError(p0: String?) { - AnalyticsUtil.logErrorAndReportToFirebase(BountyListFragment::class.java.simpleName, "Failed to update action configs: $p0", null) - } + private fun updateActionConfig() = lifecycleScope.launch { + Hover.initialize(requireActivity(), object : Hover.DownloadListener { + override fun onError(p0: String?) { + AnalyticsUtil.logErrorAndReportToFirebase(BountyListFragment::class.java.simpleName, "Failed to update action configs: $p0", null) + } - override fun onSuccess(p0: ArrayList?) { - Timber.i("Action configs initialized successfully $p0") - } - }) + override fun onSuccess(p0: ArrayList?) { + Timber.i("Action configs initialized successfully $p0") + } + }) + } private fun updateChannelsWorker() = with(WorkManager.getInstance(requireActivity())) { beginUniqueWork(UpdateChannelsWorker.CHANNELS_WORK_ID, ExistingWorkPolicy.REPLACE, UpdateChannelsWorker.makeWork()).enqueue() @@ -115,85 +116,17 @@ class BountyListFragment : Fragment(), BountyListItem.SelectListener, CountryAda dialog!!.showIt() } - private fun initCountryDropdown(countryCodes: List) = binding.bountyCountryDropdown.apply { - setListener(this@BountyListFragment) - updateChoices(countryCodes, bountyViewModel.country) - isEnabled = true - } - - private fun initRecyclerView() { - binding.bountiesRecyclerView.layoutManager = UIHelper.setMainLinearManagers(context) - } - - private fun startObservers() = with(bountyViewModel) { - val actionsObserver = object : Observer> { - override fun onChanged(t: List?) { - Timber.v("Actions update: ${t?.size}") - } - } - - val txnObserver = object : Observer> { - override fun onChanged(t: List?) { - Timber.v("Transactions update ${t?.size}") + private fun initBountyList() { + binding.bountyList.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + BountyList(bountyViewModel = bountiesViewModel) } } - - val simsObserver = object: Observer> { - override fun onChanged(t: List?) { - Timber.v("Sims update ${t?.size}") - } - } - - actions.observe(viewLifecycleOwner, actionsObserver) - transactions.observe(viewLifecycleOwner, txnObserver) - sims.observe(viewLifecycleOwner, simsObserver) - bounties.observe(viewLifecycleOwner) { updateChannelList(channels.value, it) } - channels.observe(viewLifecycleOwner) { updateChannelList(it, bounties.value)} - channelCountryList.observe(viewLifecycleOwner) { initCountryDropdown(it) } } - private fun updateChannelList(channels: List?, bounties: List?) { - binding.countryFilter.hideProgressIndicator() - - if (!channels.isNullOrEmpty() && !bounties.isNullOrEmpty() && - bountyViewModel.country == CountryAdapter.CODE_ALL_COUNTRIES || channels?.firstOrNull()?.countryAlpha2 == bountyViewModel.country - ) { - hideLoadingState() - - binding.msgNoBounties.visibility = View.GONE - binding.bountiesRecyclerView.visibility = View.VISIBLE - - showBounties(channels, bounties!!) - } else { - binding.msgNoBounties.visibility = View.VISIBLE - binding.bountiesRecyclerView.visibility = View.GONE - } - } - - private fun showBounties(channels: List, bounties: List) = lifecycleScope.launch { - val openBounties = bounties.filter { it.action.bounty_is_open || it.transactionCount != 0 } - - val channelBounties = channels.filter { c -> - openBounties.any { it.action.channel_id == c.id } - }.map { channel -> - ChannelBounties(channel, openBounties.filter { it.action.channel_id == channel.id }) - } - - bountyAdapter.submitList(channelBounties) - } - - override fun viewTransactionDetail(uuid: String?) { - uuid?.let { NavUtil.showTransactionDetailsFragment(findNavController(), uuid) } - } - - override fun viewBountyDetail(b: Bounty) { - if (bountyViewModel.isSimPresent(b)) showBountyDescDialog(b) else showSimErrorDialog(b) - } - - override fun countrySelect(countryCode: String) { - showLoadingState() - - bountyViewModel.filterChannels(countryCode).observe(viewLifecycleOwner) { updateChannelList(it, bountyViewModel.bounties.value) } + private fun viewBountyDetail(b: Bounty) { + if (bountiesViewModel.isSimPresent(b)) showBountyDescDialog(b) else showSimErrorDialog(b) } private fun showSimErrorDialog(b: Bounty) { @@ -223,22 +156,8 @@ class BountyListFragment : Fragment(), BountyListItem.SelectListener, CountryAda (requireActivity() as AbstractHoverCallerActivity).makeRegularCall(b.action, R.string.clicked_start_bounty) } - private fun showLoadingState() { - binding.bountyCountryDropdown.setState(getString(R.string.filtering_in_progress), AbstractStatefulInput.INFO) - binding.bountiesRecyclerView.visibility = View.GONE - } - - private fun hideLoadingState() { - binding.bountyCountryDropdown.setState(null, AbstractStatefulInput.NONE) - binding.bountiesRecyclerView.visibility = View.VISIBLE - } - private fun retrySimMatch(b: Bounty?) { - with(bountyViewModel.sims) { - removeObservers(viewLifecycleOwner) - observe(viewLifecycleOwner) { b?.let { viewBountyDetail(b) } } - } - + b?.let { viewBountyDetail(b) } Hover.updateSimInfo(requireActivity()) } diff --git a/app/src/main/java/com/hover/stax/presentation/bounties/BountyScreen.kt b/app/src/main/java/com/hover/stax/presentation/bounties/BountyScreen.kt new file mode 100644 index 000000000..b6c8d9b46 --- /dev/null +++ b/app/src/main/java/com/hover/stax/presentation/bounties/BountyScreen.kt @@ -0,0 +1,477 @@ +package com.hover.stax.presentation.bounties + +import android.content.Context +import android.text.Html +import android.text.method.LinkMovementMethod +import android.view.Gravity +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.toSize +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.text.HtmlCompat +import androidx.core.widget.TextViewCompat +import com.hover.stax.R +import com.hover.stax.countries.CountryAdapter +import com.hover.stax.domain.model.Bounty +import com.hover.stax.domain.model.ChannelBounties +import com.hover.stax.ui.theme.Brutalista +import com.hover.stax.ui.theme.StaxTheme +import com.yariksoffice.lingver.Lingver +import java.util.* + +const val CODE_ALL_COUNTRIES = "00" + +@Composable +fun BountyList(bountyViewModel: BountyViewModel) { + val bountiesState by bountyViewModel.bountiesState.collectAsState() + val countries by bountyViewModel.countryList.collectAsState(initial = listOf(CODE_ALL_COUNTRIES)) + val country by bountyViewModel.country.collectAsState() + + StaxTheme { + Surface( + modifier = Modifier + .fillMaxSize(), color = MaterialTheme.colors.background + ) { + LazyColumn { + item { + CountryDropdown(countries, country, bountiesState.loading, bountyViewModel) + } + + items(bountiesState.bounties) { + ChannelBountyCard(channelBounty = it, bountyViewModel) + } + + if (bountiesState.bounties.isEmpty() && !bountiesState.loading) { + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(dimensionResource(id = R.dimen.margin_13)), + text = stringResource(id = R.string.bounty_error_none), + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center + ) + } + } + } + } + } +} + +@Composable +fun ChannelBountyCard(channelBounty: ChannelBounties, bountyViewModel: BountyViewModel) { + if (channelBounty.bounties.isNotEmpty()) + Column { + Text( + text = channelBounty.channel.ussdName.uppercase(), + modifier = Modifier + .fillMaxWidth() + .padding(dimensionResource(id = R.dimen.margin_13)), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.End + ) + + channelBounty.bounties.forEach { + BountyCard(bounty = it, bountyViewModel) + } + } +} + +@Composable +fun BountyCard(bounty: Bounty, bountyViewModel: BountyViewModel) { + val context = LocalContext.current + val margin8 = dimensionResource(id = R.dimen.margin_8) + val margin13 = dimensionResource(id = R.dimen.margin_13) + val margin5 = dimensionResource(id = R.dimen.margin_5) + + val bountyState = getBountyState(bounty) + + val strikeThrough = TextStyle( + fontFamily = Brutalista, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + textDecoration = TextDecoration.LineThrough + ) + + Column( + modifier = Modifier + .background(color = colorResource(id = bountyState.color)) + .padding(vertical = margin8) + .clickable { bountyState.bountySelectEvent?.let { bountyViewModel.handleBountyEvent(it) } } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = margin13), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = bounty.generateDescription(context).replaceFirstChar { it.uppercase() }, + modifier = Modifier + .padding(top = margin8, bottom = margin8, end = margin13) + .weight(1f), + style = if (bountyState.isOpen) MaterialTheme.typography.body1 else strikeThrough + ) + + Text( + text = stringResource(R.string.bounty_amount_with_currency, bounty.action.bounty_amount), + modifier = Modifier + .padding(top = margin8, bottom = margin8), + style = if (bountyState.isOpen) MaterialTheme.typography.body1 else strikeThrough, + fontWeight = FontWeight.Medium + ) + } + + if (bountyState.msg != 0) + SpannableImageTextView( + drawable = bountyState.icon, + stringRes = bountyState.msg, + modifier = Modifier + .padding(start = margin13, end = margin13, top = margin5, bottom = margin5), + ) + } +} + +@Composable +internal fun SpannableImageTextView( + @DrawableRes drawable: Int, + @StringRes stringRes: Int, + modifier: Modifier = Modifier +) { + val text = HtmlCompat.fromHtml(stringResource(id = stringRes), HtmlCompat.FROM_HTML_MODE_LEGACY).toString() + + Row(horizontalArrangement = Arrangement.Start, modifier = modifier) { + Image( + painter = painterResource(id = drawable), + contentDescription = null, + modifier = Modifier.align(Alignment.CenterVertically), + ) + //only workaround available for handling html text in compose textviews + AndroidView( + factory = { context -> + TextView(context).apply { + setText(text) + setTextColor(R.color.offWhite) + TextViewCompat.setTextAppearance(this, android.R.style.TextAppearance_Material_Caption) + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + movementMethod = LinkMovementMethod.getInstance() + gravity = Gravity.CENTER_VERTICAL + typeface = context.resources.getFont(R.font.brutalista_regular) + } + }, modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = dimensionResource(id = R.dimen.margin_8)) + ) + } +} + +@Composable +fun CountryDropdown(countries: List, country: String, isLoading: Boolean, bountyViewModel: BountyViewModel) { + var expanded by remember { mutableStateOf(false) } + var selected by remember { mutableStateOf(country) } + var textFieldSize by remember { mutableStateOf(Size.Zero) } + val interactionSource = remember { MutableInteractionSource() } + + val icon = if (expanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown + val borderColor = if (isLoading) colorResource(id = R.color.stax_state_blue) else Color.White + + val context = LocalContext.current + + if (interactionSource.collectIsPressedAsState().value) + expanded = !expanded + + Column( + Modifier + .padding(10.dp) + ) { + OutlinedTextField( + value = getCountryString(selected, context), + onValueChange = { selected = it }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = dimensionResource(id = R.dimen.margin_10)) + .onGloballyPositioned { coordinates -> + textFieldSize = coordinates.size.toSize() + }, + label = { + Text(stringResource(id = R.string.select_country), style = MaterialTheme.typography.body1) + }, + trailingIcon = { + Icon( + icon, + "Dropdown", + Modifier.clickable { expanded = !expanded }, + tint = if (isLoading) colorResource(id = R.color.stax_state_blue) else Color.White + ) + }, + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedLabelColor = borderColor, + unfocusedBorderColor = borderColor, + focusedBorderColor = borderColor, + unfocusedLabelColor = borderColor + ), + readOnly = true, + interactionSource = interactionSource + ) + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier + .width(with(LocalDensity.current) { textFieldSize.width.toDp() }) + ) { + countries.forEach { countryCode -> + DropdownMenuItem(onClick = { + selected = countryCode + expanded = false + bountyViewModel.loadBounties(countryCode) + }) { + Text( + modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.margin_10)), + text = getCountryString(countryCode, context), + style = MaterialTheme.typography.button + ) + } + } + } + + if (isLoading) + Text( + modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.margin_10)), + text = stringResource(id = R.string.filtering_in_progress), + style = MaterialTheme.typography.body2, + color = colorResource(id = R.color.stax_state_blue) + ) + } +} + +@Preview +@Composable +fun ChannelBountiesCardPreview() { + Column { + Text( + text = "ACS Microfinance - *614*435# - NG".uppercase(), + modifier = Modifier + .fillMaxWidth() + .padding(dimensionResource(id = R.dimen.margin_13)), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.End + ) + + repeat(3) { + BountyCardPreview() + } + } +} + +@Preview +@Composable +fun BountyCardPreview() { + val margin13 = dimensionResource(id = R.dimen.margin_13) + val margin8 = dimensionResource(id = R.dimen.margin_8) + + Column( + modifier = Modifier + .background(color = colorResource(id = R.color.colorSurface)) + .padding(vertical = margin8) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = margin13), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Check Balance", + modifier = Modifier + .padding(top = margin8, bottom = margin8, end = margin13), + style = MaterialTheme.typography.body1 + ) + + Text( + text = "USD $1", + modifier = Modifier + .padding(top = margin8, bottom = margin8), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Medium + ) + } + + SpannableImageTextView( + drawable = R.drawable.ic_error, + stringRes = R.string.bounty_transaction_failed, + modifier = Modifier.padding(start = margin8, end = margin13, top = 5.dp, bottom = dimensionResource(id = R.dimen.margin_10)), + ) + } +} + +@Preview +@Composable +fun CountryDropdownPreview() { + val countryCodes = listOf("KE, UG, TZ, ET, ZA") + var expanded by remember { mutableStateOf(false) } + var selected by remember { mutableStateOf(CODE_ALL_COUNTRIES) } + var textFieldSize by remember { mutableStateOf(Size.Zero) } + + val icon = if (expanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown + + Column(Modifier.padding(10.dp)) { + OutlinedTextField( + value = selected, + onValueChange = { selected = it }, + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { coordinates -> + textFieldSize = coordinates.size.toSize() + }, + label = { + Text( + text = stringResource(id = R.string.select_country), + style = MaterialTheme.typography.body2, + ) + }, + trailingIcon = { + Icon( + icon, + "contentDescription", + Modifier.clickable { expanded = !expanded }, + tint = Color.White + ) + }, + readOnly = true, + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedLabelColor = colorResource(id = R.color.stax_state_blue), + unfocusedBorderColor = Color.White, + focusedBorderColor = colorResource(id = R.color.stax_state_blue), + unfocusedLabelColor = Color.White + ) + ) + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier + .width(with(LocalDensity.current) { textFieldSize.width.toDp() }) + ) { + countryCodes.forEach { countryCode -> + DropdownMenuItem(onClick = { + selected = countryCode + expanded = false + }) { + Text(text = getCountryString(countryCode, LocalContext.current)) + } + } + } + + Text( + modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.margin_10)), + text = stringResource(id = R.string.filtering_in_progress), + style = MaterialTheme.typography.body2, + color = colorResource(id = R.color.stax_state_blue) + ) + } +} + +@Preview +@Composable +fun BountiesPreview() { + StaxTheme { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + LazyColumn { + item { + CountryDropdownPreview() + } + items(5) { + ChannelBountiesCardPreview() + } + } + } + } +} + +private fun getBountyState(bounty: Bounty): BountyItemState { + return when { + bounty.hasSuccessfulTransactions() -> + BountyItemState(color = R.color.muted_green, msg = R.string.done, icon = R.drawable.ic_check, isOpen = false, bountySelectEvent = null) + bounty.isLastTransactionFailed() && !bounty.action.bounty_is_open -> + BountyItemState(color = R.color.stax_bounty_red_bg, msg = R.string.bounty_transaction_failed, icon = R.drawable.ic_error, isOpen = false, bountySelectEvent = BountySelectEvent.ViewTransactionDetail(bounty.transactions.last().uuid)) + bounty.isLastTransactionFailed() && bounty.action.bounty_is_open -> + BountyItemState(color = R.color.stax_bounty_red_bg, msg = R.string.bounty_transaction_failed_try_again, icon = R.drawable.ic_error, isOpen = true, bountySelectEvent = BountySelectEvent.ViewBountyDetail(bounty)) + !bounty.action.bounty_is_open -> + BountyItemState(color = R.color.lighter_grey, isOpen = false, bountySelectEvent = null) + bounty.transactionCount > 0 -> + BountyItemState(color = R.color.pending_brown, msg = R.string.bounty_pending_short_desc, icon = R.drawable.ic_warning, isOpen = true, bountySelectEvent = BountySelectEvent.ViewTransactionDetail(bounty.transactions.last().uuid)) + else -> + BountyItemState(color = R.color.colorSurface, isOpen = true, bountySelectEvent = BountySelectEvent.ViewBountyDetail(bounty)) + } +} + +fun getCountryString(code: String, context: Context): String = if (code.isEmpty() || code == CountryAdapter.CODE_ALL_COUNTRIES) + context.getString(R.string.all_countries_with_emoji) +else + context.getString(R.string.country_with_emoji, countryCodeToEmoji(code), getFullCountryName(code)) + +private fun getFullCountryName(code: String): String { + val locale = Locale(Lingver.getInstance().getLanguage(), code) + return locale.displayCountry +} + +private fun countryCodeToEmoji(countryCode: String): String { + return try { + val firstLetter = Character.codePointAt(countryCode.uppercase(), 0) - 0x41 + 0x1F1E6 + val secondLetter = Character.codePointAt(countryCode.uppercase(), 1) - 0x41 + 0x1F1E6 + String(Character.toChars(firstLetter)) + String(Character.toChars(secondLetter)) + } catch (e: Exception) { + "" + } +} + +data class BountyItemState( + @ColorRes val color: Int = 0, + @StringRes val msg: Int = 0, + @DrawableRes val icon: Int = 0, + val isOpen: Boolean = true, + val bountySelectEvent: BountySelectEvent? = null +) + +sealed class BountySelectEvent { + data class ViewTransactionDetail(val uuid: String) : BountySelectEvent() + data class ViewBountyDetail(val bounty: Bounty) : BountySelectEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/financialTips/FinancialTipsAdapter.kt b/app/src/main/java/com/hover/stax/presentation/financial_tips/FinancialTipsAdapter.kt similarity index 93% rename from app/src/main/java/com/hover/stax/financialTips/FinancialTipsAdapter.kt rename to app/src/main/java/com/hover/stax/presentation/financial_tips/FinancialTipsAdapter.kt index 841346323..55858bd96 100644 --- a/app/src/main/java/com/hover/stax/financialTips/FinancialTipsAdapter.kt +++ b/app/src/main/java/com/hover/stax/presentation/financial_tips/FinancialTipsAdapter.kt @@ -1,9 +1,10 @@ -package com.hover.stax.financialTips +package com.hover.stax.presentation.financial_tips import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.hover.stax.databinding.ItemWellnessTipsBinding +import com.hover.stax.domain.model.FinancialTip import com.hover.stax.utils.DateUtils import timber.log.Timber import java.util.* diff --git a/app/src/main/java/com/hover/stax/financialTips/FinancialTipsFragment.kt b/app/src/main/java/com/hover/stax/presentation/financial_tips/FinancialTipsFragment.kt similarity index 77% rename from app/src/main/java/com/hover/stax/financialTips/FinancialTipsFragment.kt rename to app/src/main/java/com/hover/stax/presentation/financial_tips/FinancialTipsFragment.kt index f9e16e9e8..61c4d05f4 100644 --- a/app/src/main/java/com/hover/stax/financialTips/FinancialTipsFragment.kt +++ b/app/src/main/java/com/hover/stax/presentation/financial_tips/FinancialTipsFragment.kt @@ -1,4 +1,4 @@ -package com.hover.stax.financialTips +package com.hover.stax.presentation.financial_tips import android.content.Intent import android.os.Bundle @@ -13,9 +13,10 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.hover.stax.R import com.hover.stax.databinding.FragmentWellnessBinding +import com.hover.stax.domain.model.FinancialTip import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.UIHelper -import com.hover.stax.utils.collectLatestLifecycleFlow +import com.hover.stax.utils.collectLifecycleFlow import org.json.JSONObject import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber @@ -42,8 +43,7 @@ class FinancialTipsFragment : Fragment(), FinancialTipsAdapter.SelectListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.title.text = getString(R.string.financial_wellness_tips) - binding.backButton.setOnClickListener { findNavController().popBackStack() } + initViews() viewModel.getTips() @@ -51,11 +51,41 @@ class FinancialTipsFragment : Fragment(), FinancialTipsAdapter.SelectListener { requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedCallback) } - private fun startObserver() = collectLatestLifecycleFlow(viewModel.tipState) { - showFinancialTips(it.tips, args.tipId) + private fun initViews(){ + binding.title.text = getString(R.string.financial_wellness_tips) + binding.backButton.setOnClickListener { findNavController().popBackStack() } + binding.progressIndicator.setVisibilityAfterHide(View.GONE) + } + + private fun startObserver() = collectLifecycleFlow(viewModel.tipsState) { + when { + it.isLoading -> { + binding.progressIndicator.show() + binding.empty.visibility = View.GONE + } + it.tips.isEmpty() && !it.isLoading-> { + binding.progressIndicator.hide() + binding.empty.visibility = View.VISIBLE + binding.financialTips.visibility = View.GONE + binding.financialTipsDetail.visibility = View.GONE + } + it.tips.isNotEmpty() -> { + binding.progressIndicator.hide() + showFinancialTips(it.tips, args.tipId) + } + it.error.isNotEmpty() -> { + binding.progressIndicator.hide() + binding.empty.visibility = View.VISIBLE + binding.financialTips.visibility = View.GONE + binding.financialTipsDetail.visibility = View.GONE + } + } } private fun showFinancialTips(tips: List, id: String? = null) { + binding.empty.visibility = View.GONE + binding.financialTips.visibility = View.VISIBLE + if (id != null) { tips.firstOrNull { it.id == id }?.let { onTipSelected(it, true) } } else { @@ -154,8 +184,8 @@ class FinancialTipsFragment : Fragment(), FinancialTipsAdapter.SelectListener { binding.financialTipsDetail.visibility = View.GONE binding.tipsCard.visibility = View.VISIBLE - if (viewModel.tipState.value.tips.isNotEmpty()) - showFinancialTips(viewModel.tipState.value.tips, null) + if (viewModel.tipsState.value.tips.isNotEmpty()) + showFinancialTips(viewModel.tipsState.value.tips, null) } override fun onDestroyView() { diff --git a/app/src/main/java/com/hover/stax/presentation/financial_tips/FinancialTipsState.kt b/app/src/main/java/com/hover/stax/presentation/financial_tips/FinancialTipsState.kt new file mode 100644 index 000000000..405acf538 --- /dev/null +++ b/app/src/main/java/com/hover/stax/presentation/financial_tips/FinancialTipsState.kt @@ -0,0 +1,9 @@ +package com.hover.stax.presentation.financial_tips + +import com.hover.stax.domain.model.FinancialTip + +data class FinancialTipsState( + val isLoading: Boolean = false, + val tips: List = emptyList(), + val error: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/presentation/financial_tips/FinancialTipsViewModel.kt b/app/src/main/java/com/hover/stax/presentation/financial_tips/FinancialTipsViewModel.kt new file mode 100644 index 000000000..2ad9ff9ef --- /dev/null +++ b/app/src/main/java/com/hover/stax/presentation/financial_tips/FinancialTipsViewModel.kt @@ -0,0 +1,29 @@ +package com.hover.stax.presentation.financial_tips + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hover.stax.domain.model.Resource +import com.hover.stax.domain.use_case.financial_tips.TipsUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class FinancialTipsViewModel(private val tipsUseCase: TipsUseCase) : ViewModel() { + + + private val _tipsState = MutableStateFlow(FinancialTipsState()) + val tipsState = _tipsState.asStateFlow() + + init { + getTips() + } + + fun getTips() = tipsUseCase().onEach { result -> + when (result) { + is Resource.Loading -> _tipsState.value = FinancialTipsState(isLoading = true) + is Resource.Error -> _tipsState.value = FinancialTipsState(error = result.message ?: "An unexpected error occurred", isLoading = false) + is Resource.Success -> _tipsState.value = FinancialTipsState(tips = result.data ?: emptyList(), isLoading = false) + } + }.launchIn(viewModelScope) +} diff --git a/app/src/main/java/com/hover/stax/presentation/home/BalanceScreen.kt b/app/src/main/java/com/hover/stax/presentation/home/BalanceScreen.kt new file mode 100644 index 000000000..14a4f732b --- /dev/null +++ b/app/src/main/java/com/hover/stax/presentation/home/BalanceScreen.kt @@ -0,0 +1,238 @@ +package com.hover.stax.presentation.home + +import android.content.Context +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* +import androidx.compose.material.MaterialTheme.colors +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.CachePolicy +import coil.request.ImageRequest +import com.hover.stax.R +import com.hover.stax.domain.model.Account +import com.hover.stax.ui.theme.ColorSurface +import com.hover.stax.ui.theme.DarkGray +import com.hover.stax.ui.theme.OffWhite +import com.hover.stax.ui.theme.StaxTheme +import com.hover.stax.utils.DateUtils + + +interface BalanceTapListener { + fun onTapBalanceRefresh(account: Account?) + fun onTapBalanceDetail(accountId: Int) +} + +@Composable +fun BalanceHeader(onClickedAddAccount: () -> Unit, accountExists: Boolean) { + val size13 = dimensionResource(id = R.dimen.margin_13) + + Row( + modifier = Modifier + .padding(all = size13) + .fillMaxWidth() + ) { + Text( + text = stringResource(id = R.string.your_accounts), + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.h4 + ) + + if (accountExists) { + Text( + text = stringResource(id = R.string.add_an_account), + style = MaterialTheme.typography.body2, + modifier = Modifier + .clickable(onClick = onClickedAddAccount) + .padding(end = 5.dp) + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Icon( + painter = painterResource(id = R.drawable.ic_add_white_16), + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .clickable(onClick = onClickedAddAccount) + .background(color = colorResource(id = R.color.brightBlue)) + ) + } + } +} + +@Composable +fun EmptyBalance(onClickedAddAccount: () -> Unit) { + val size34 = dimensionResource(id = R.dimen.margin_34) + val size16 = dimensionResource(id = R.dimen.margin_16) + Column(modifier = Modifier.padding(vertical = size16)) { + val modifier = Modifier.padding(horizontal = size34) + + Text( + text = stringResource(id = R.string.your_accounts), + style = MaterialTheme.typography.h4, + modifier = Modifier.padding(horizontal = size16) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(id = R.string.empty_balance_desc), + style = MaterialTheme.typography.body1, + color = colorResource(id = R.color.offWhite), + textAlign = TextAlign.Center, + modifier = modifier + ) + + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedButton( + onClick = onClickedAddAccount, + modifier = Modifier + .fillMaxWidth() + .shadow(elevation = 0.dp) + .then(modifier), + shape = MaterialTheme.shapes.medium, + border = BorderStroke(width = 0.5.dp, color = DarkGray), + colors = ButtonDefaults.buttonColors( + backgroundColor = ColorSurface, + contentColor = OffWhite + ) + ) { + Text( + text = stringResource(id = R.string.add_account), + style = MaterialTheme.typography.button, + modifier = Modifier + .fillMaxWidth() + .padding(top = 5.dp, bottom = 5.dp), + textAlign = TextAlign.Center + ) + } + } +} + +@Composable +fun BalanceItem(staxAccount: Account, balanceTapListener: BalanceTapListener?, context: Context) { + val size34 = dimensionResource(id = R.dimen.margin_34) + val size13 = dimensionResource(id = R.dimen.margin_13) + Column { + Row(modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 13.dp) + .heightIn(min = 70.dp) + .clickable { balanceTapListener?.onTapBalanceDetail(accountId = staxAccount.id) }) { + + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(staxAccount.logoUrl) + .crossfade(true) + .diskCachePolicy(CachePolicy.ENABLED) + .build(), + contentDescription = "", + placeholder = painterResource(id = R.drawable.img_placeholder), + error = painterResource(id = R.drawable.img_placeholder), + modifier = Modifier + .size(size34) + .clip(CircleShape) + .align(Alignment.CenterVertically), + contentScale = ContentScale.Crop + ) + + Text( + text = staxAccount.alias, + style = MaterialTheme.typography.body2, + modifier = Modifier + .weight(1f) + .padding(start = size13) + .align(Alignment.CenterVertically), + color = colorResource(id = R.color.white) + ) + + Column(modifier = Modifier.align(Alignment.CenterVertically)) { + Text( + text = staxAccount.latestBalance ?: " - ", + modifier = Modifier.align(Alignment.End), + style = MaterialTheme.typography.subtitle2, + color = colorResource(id = R.color.offWhite) + ) + + Spacer(modifier = Modifier.height(2.dp)) + + if (staxAccount.latestBalance != null) + Text( + text = DateUtils.timeAgo(context, staxAccount.latestBalanceTimestamp), + modifier = Modifier.align(Alignment.End), + color = colorResource(id = R.color.offWhite), + style = MaterialTheme.typography.caption + ) + } + + Image(painter = painterResource(id = R.drawable.ic_refresh_white_24dp), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = size13) + .clickable { balanceTapListener?.onTapBalanceRefresh(staxAccount) } + .size(32.dp) + ) + + } + + Divider( + color = colorResource(id = R.color.nav_grey), + modifier = Modifier.padding(horizontal = 13.dp) + ) + } +} + +@Preview +@Composable +fun BalanceScreenPreview() { + StaxTheme { + Surface { + BalanceListForPreview(accountList = emptyList()) + } + } +} + +@Composable +private fun BalanceListForPreview(accountList: List) { + + if (accountList.isEmpty()) { + EmptyBalance {} + } else { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(13.dp) + ) { + item { + BalanceHeader(onClickedAddAccount = {}, accountExists = false) + } + items(accountList) { account -> + val context = LocalContext.current + BalanceItem(staxAccount = account, context = context, balanceTapListener = null) + } + } + } +} + diff --git a/app/src/main/java/com/hover/stax/balances/BalancesViewModel.kt b/app/src/main/java/com/hover/stax/presentation/home/BalancesViewModel.kt similarity index 67% rename from app/src/main/java/com/hover/stax/balances/BalancesViewModel.kt rename to app/src/main/java/com/hover/stax/presentation/home/BalancesViewModel.kt index efd949587..ed2697a7d 100644 --- a/app/src/main/java/com/hover/stax/balances/BalancesViewModel.kt +++ b/app/src/main/java/com/hover/stax/presentation/home/BalancesViewModel.kt @@ -1,4 +1,4 @@ -package com.hover.stax.balances +package com.hover.stax.presentation.home import android.app.Application import android.content.Context @@ -8,24 +8,21 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.hover.sdk.actions.HoverAction import com.hover.stax.R -import com.hover.stax.accounts.Account -import com.hover.stax.accounts.AccountRepo -import com.hover.stax.accounts.PLACEHOLDER -import com.hover.stax.actions.ActionRepo -import com.hover.stax.utils.Utils +import com.hover.stax.data.local.accounts.AccountRepo +import com.hover.stax.data.local.actions.ActionRepo + +import com.hover.stax.domain.model.Account +import com.hover.stax.domain.model.PLACEHOLDER + import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch class BalancesViewModel(application: Application, val actionRepo: ActionRepo, val accountRepo: AccountRepo) : AndroidViewModel(application) { - private var _showBalances = MutableLiveData(false) - val showBalances: LiveData = _showBalances - var userRequestedBalanceAccount = MutableLiveData() private var _balanceAction = MutableSharedFlow() @@ -38,24 +35,19 @@ class BalancesViewModel(application: Application, val actionRepo: ActionRepo, va val actionRunError = _actionRunError.receiveAsFlow() init { - _showBalances.value = Utils.getBoolean(BalancesFragment.BALANCE_VISIBILITY_KEY, getApplication(), true) - getAccounts() } - fun setBalanceState(show: Boolean) = viewModelScope.launch { - Utils.saveBoolean(BalancesFragment.BALANCE_VISIBILITY_KEY, show, getApplication()) - _showBalances.postValue(show) - } - fun requestBalance(account: Account) { userRequestedBalanceAccount.value = account startBalanceActionFor(userRequestedBalanceAccount.value) } private fun startBalanceActionFor(account: Account?) = viewModelScope.launch(Dispatchers.IO) { - val channelId = account?.channelId ?: -1 - val action = actionRepo.getActions(channelId, if (account?.name == PLACEHOLDER) HoverAction.FETCH_ACCOUNTS else HoverAction.BALANCE).firstOrNull() + if(account == null) return@launch + + val channelId = account.channelId + val action = actionRepo.getActions(channelId, if (account.name.contains(PLACEHOLDER)) HoverAction.FETCH_ACCOUNTS else HoverAction.BALANCE).firstOrNull() action?.let { _balanceAction.emit(action) } ?: run { _actionRunError.send((getApplication() as Context).getString(R.string.error_running_action)) } } diff --git a/app/src/main/java/com/hover/stax/presentation/home/HomeFragment.kt b/app/src/main/java/com/hover/stax/presentation/home/HomeFragment.kt new file mode 100644 index 000000000..c2fe8a3d5 --- /dev/null +++ b/app/src/main/java/com/hover/stax/presentation/home/HomeFragment.kt @@ -0,0 +1,149 @@ +package com.hover.stax.presentation.home + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import androidx.navigation.NavDirections +import androidx.navigation.fragment.findNavController +import com.hover.sdk.actions.HoverAction +import com.hover.stax.MainNavigationDirections +import com.hover.stax.R +import com.hover.stax.addChannels.ChannelsViewModel +import com.hover.stax.bonus.BonusViewModel +import com.hover.stax.databinding.FragmentHomeBinding +import com.hover.stax.domain.model.Account +import com.hover.stax.home.MainActivity +import com.hover.stax.hover.AbstractHoverCallerActivity +import com.hover.stax.utils.* +import com.hover.stax.views.StaxDialog +import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber + + +class HomeFragment : Fragment(), FinancialTipClickInterface, BalanceTapListener { + + private var _binding: FragmentHomeBinding? = null + private val binding get() = _binding!! + + private val bonusViewModel: BonusViewModel by sharedViewModel() + private val channelsViewModel: ChannelsViewModel by sharedViewModel() + private val balancesViewModel: BalancesViewModel by sharedViewModel() + private val homeViewModel: HomeViewModel by viewModel() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + AnalyticsUtil.logAnalyticsEvent(getString(R.string.visit_screen, getString(R.string.visit_home)), requireContext()) + _binding = FragmentHomeBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setComposeView() + + observeForBalances() + observeForBonus() + } + + private fun getHomeClickFunctions(): HomeClickFunctions { + fun onSendMoneyClicked() = navigateTo(getTransferDirection(HoverAction.P2P)) + fun onBuyAirtimeClicked() = navigateTo(getTransferDirection(HoverAction.AIRTIME)) + fun onBuyGoodsClicked() = navigateTo(HomeFragmentDirections.actionNavigationHomeToMerchantFragment()) + fun onPayBillClicked() = navigateTo(HomeFragmentDirections.actionNavigationHomeToPaybillFragment()) + fun onRequestMoneyClicked() = navigateTo(HomeFragmentDirections.actionNavigationHomeToNavigationRequest()) + fun onClickedAddNewAccount() = (requireActivity() as MainActivity).checkPermissionsAndNavigate(MainNavigationDirections.actionGlobalAddChannelsFragment()) + fun onClickedTermsAndConditions() = Utils.openUrl(getString(R.string.terms_and_condition_url), requireContext()) + fun onClickedSettingsIcon() = navigateTo(HomeFragmentDirections.toSettingsFragment()) + + return HomeClickFunctions( + onSendMoneyClicked = { onSendMoneyClicked() }, + onBuyAirtimeClicked = { onBuyAirtimeClicked() }, + onBuyGoodsClicked = { onBuyGoodsClicked() }, + onPayBillClicked = { onPayBillClicked() }, + onRequestMoneyClicked = { onRequestMoneyClicked() }, + onClickedAddNewAccount = { onClickedAddNewAccount() }, + onClickedTC = { onClickedTermsAndConditions() }, + onClickedSettingsIcon = { onClickedSettingsIcon() } + ) + } + + private fun setComposeView() { + binding.root.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + binding.root.setContent { + HomeScreen( + channelsViewModel, + homeClickFunctions = getHomeClickFunctions(), + tipInterface = this@HomeFragment, + balanceTapListener = this@HomeFragment, + homeViewModel = homeViewModel + ) + } + } + + private fun observeForBonus() { + collectLifecycleFlow(channelsViewModel.accountEventFlow) { + navigateTo(getTransferDirection(HoverAction.AIRTIME, bonusViewModel.bonusList.value.bonuses.first().userChannel.toString())) + } + } + + private fun observeForBalances() { + collectLifecycleFlow(balancesViewModel.balanceAction) { + attemptCallHover(balancesViewModel.userRequestedBalanceAccount.value, it) + } + + collectLifecycleFlow(channelsViewModel.accountCallback) { + askToCheckBalance(it) + } + + collectLifecycleFlow(balancesViewModel.actionRunError) { + UIHelper.flashAndReportError(requireActivity(), it) + } + } + + private fun getTransferDirection(type: String, channelId: String? = null): NavDirections { + return HomeFragmentDirections.actionNavigationHomeToNavigationTransfer(type).also { + if (channelId != null) it.channelId = channelId + } + } + + private fun attemptCallHover(account: Account?, action: HoverAction?) { + action?.let { account?.let { callHover(account, action) } } + } + + private fun callHover(account: Account, action: HoverAction) { + (requireActivity() as AbstractHoverCallerActivity).runSession(account, action) + } + + private fun askToCheckBalance(account: Account) { + val dialog = StaxDialog(requireActivity()).setDialogTitle(R.string.check_balance_title) + .setDialogMessage(R.string.check_balance_desc).setNegButton(R.string.later, null) + .setPosButton(R.string.check_balance_title) { onTapBalanceRefresh(account) } + dialog.showIt() + } + + private fun navigateTo(navDirections: NavDirections) = (requireActivity() as MainActivity).checkPermissionsAndNavigate(navDirections) + + override fun onTipClicked(tipId: String?) { + NavUtil.navigate(findNavController(), HomeFragmentDirections.actionNavigationHomeToWellnessFragment(tipId)) + } + + override fun onTapBalanceRefresh(account: Account?) { + if (account != null) { + AnalyticsUtil.logAnalyticsEvent(getString(R.string.refresh_balance_single), requireContext()) + balancesViewModel.requestBalance(account) + } + } + + override fun onTapBalanceDetail(accountId: Int) { + findNavController().navigate(HomeFragmentDirections.actionNavigationHomeToAccountDetailsFragment(accountId)) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/presentation/home/HomeScreen.kt b/app/src/main/java/com/hover/stax/presentation/home/HomeScreen.kt new file mode 100644 index 000000000..410a2a969 --- /dev/null +++ b/app/src/main/java/com/hover/stax/presentation/home/HomeScreen.kt @@ -0,0 +1,463 @@ +package com.hover.stax.presentation.home + +import android.annotation.SuppressLint +import android.content.Context +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.hover.stax.R +import com.hover.stax.addChannels.ChannelsViewModel +import com.hover.stax.domain.model.Bonus +import com.hover.stax.domain.model.FinancialTip +import com.hover.stax.ui.theme.StaxTheme +import com.hover.stax.utils.AnalyticsUtil +import com.hover.stax.utils.network.NetworkMonitor + +data class HomeClickFunctions( + val onSendMoneyClicked: () -> Unit, + val onBuyAirtimeClicked: () -> Unit, + val onBuyGoodsClicked: () -> Unit, + val onPayBillClicked: () -> Unit, + val onRequestMoneyClicked: () -> Unit, + val onClickedTC: () -> Unit, + val onClickedAddNewAccount: () -> Unit, + val onClickedSettingsIcon: () -> Unit +) + +interface FinancialTipClickInterface { + fun onTipClicked(tipId: String?) +} + +@Composable +fun TopBar(@StringRes title: Int = R.string.app_name, isInternetConnected: Boolean, onClickedSettingsIcon: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(all = dimensionResource(id = R.dimen.margin_13)), + ) { + HorizontalImageTextView( + drawable = R.drawable.stax_logo, + stringRes = title, + modifier = Modifier.weight(1f), + MaterialTheme.typography.button + ) + + if (!isInternetConnected) { + HorizontalImageTextView( + drawable = R.drawable.ic_internet_off, + stringRes = R.string.working_offline, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(horizontal = 16.dp), + MaterialTheme.typography.button + ) + } + + Image( + painter = painterResource(id = R.drawable.ic_settings), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterVertically) + .clickable(onClick = onClickedSettingsIcon) + .size(30.dp), + ) + } +} + +@Composable +fun BonusCard(message: String, onClickedTC: () -> Unit, onClickedTopUp: () -> Unit) { + val size13 = dimensionResource(id = R.dimen.margin_13) + val size10 = dimensionResource(id = R.dimen.margin_10) + + Card(modifier = Modifier.padding(all = size13), elevation = 2.dp) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(all = size13) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(id = R.string.get_rewarded), + style = MaterialTheme.typography.h3 + ) + Text( + text = message, + modifier = Modifier.padding(vertical = size10), + style = MaterialTheme.typography.body1 + ) + Text( + text = stringResource(id = R.string.tc_apply), + textDecoration = TextDecoration.Underline, + color = colorResource(id = R.color.brightBlue), + style = MaterialTheme.typography.body2, + modifier = Modifier.clickable(onClick = onClickedTC) + ) + Text( + text = stringResource(id = R.string.top_up), + color = colorResource(id = R.color.brightBlue), + style = MaterialTheme.typography.h4, + modifier = Modifier + .padding(top = size13) + .clickable(onClick = onClickedTopUp) + ) + } + Image( + painter = painterResource(id = R.drawable.ic_bonus), + contentDescription = stringResource(id = R.string.get_rewarded), + modifier = Modifier + .size(70.dp) + .padding(start = size13) + .align(Alignment.CenterVertically) + ) + } + } +} + +@Composable +fun PrimaryFeatures( + onSendMoneyClicked: () -> Unit, + onBuyAirtimeClicked: () -> Unit, + onBuyGoodsClicked: () -> Unit, + onPayBillClicked: () -> Unit, + onRequestMoneyClicked: () -> Unit, + showKenyaFeatures: Boolean +) { + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier + .padding(horizontal = 13.dp, vertical = 26.dp) + .fillMaxWidth() + ) { + VerticalImageTextView( + onItemClick = onSendMoneyClicked, + drawable = R.drawable.ic_send_money, + stringRes = R.string.cta_transfer + ) + VerticalImageTextView( + onItemClick = onBuyAirtimeClicked, + drawable = R.drawable.ic_system_upate_24, + stringRes = R.string.cta_airtime + ) + if (showKenyaFeatures) { + VerticalImageTextView( + onItemClick = onBuyGoodsClicked, + drawable = R.drawable.ic_shopping_cart, + stringRes = R.string.cta_merchant + ) + VerticalImageTextView( + onItemClick = onPayBillClicked, + drawable = R.drawable.ic_utility, + stringRes = R.string.cta_paybill_linebreak + ) + } + VerticalImageTextView( + onItemClick = onRequestMoneyClicked, + drawable = R.drawable.ic_baseline_people_24, + stringRes = R.string.cta_request + ) + } +} + +@Composable +private fun FinancialTipCard( + tipInterface: FinancialTipClickInterface?, + financialTip: FinancialTip, + homeViewModel: HomeViewModel? +) { + val size13 = dimensionResource(id = R.dimen.margin_13) + + Card(elevation = 0.dp, modifier = Modifier.padding(all = size13)) { + Column { + Row(modifier = Modifier + .fillMaxWidth() + .padding(all = size13)) { + HorizontalImageTextView( + drawable = R.drawable.ic_tip_of_day, + stringRes = R.string.tip_of_the_day, + Modifier.weight(1f), + MaterialTheme.typography.button + ) + + Image(painter = painterResource(id = R.drawable.ic_close_white), + contentDescription = null, + alignment = Alignment.CenterEnd, + modifier = Modifier.clickable { homeViewModel?.dismissTip(financialTip.id) }) + } + + Row(modifier = Modifier + .padding(horizontal = size13) + .clickable { tipInterface?.onTipClicked(null) }) { + + Column(modifier = Modifier.weight(1f)) { + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = financialTip.title, + style = MaterialTheme.typography.body2, + textDecoration = TextDecoration.Underline + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = financialTip.snippet, + style = MaterialTheme.typography.body2, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(bottom = size13, top = 3.dp) + ) + + Text(text = stringResource(id = R.string.read_more), + color = colorResource(id = R.color.brightBlue), + modifier = Modifier + .padding(bottom = size13) + .clickable { tipInterface?.onTipClicked(financialTip.id) } + ) + } + + Image( + painter = painterResource(id = R.drawable.tips_fancy_icon), + contentDescription = null, + modifier = Modifier + .size(60.dp) + .padding(start = size13) + .align(Alignment.CenterVertically), + ) + } + } + } +} + +@Composable +private fun VerticalImageTextView( + @DrawableRes drawable: Int, + @StringRes stringRes: Int, + onItemClick: () -> Unit +) { + val size24 = dimensionResource(id = R.dimen.margin_24) + val blue = colorResource(id = R.color.stax_state_blue) + + Column( + modifier = Modifier + .clickable(onClick = onItemClick) + .padding(horizontal = 2.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .clip(CircleShape) + .size(48.dp) + .background(blue), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = drawable), + contentDescription = "", + modifier = Modifier + .size(size24) + ) + } + + Text( + text = stringResource(id = stringRes), + color = colorResource(id = R.color.offWhite), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.caption, + modifier = Modifier + .padding(top = dimensionResource(id = R.dimen.margin_16)) + .widthIn(min = 50.dp, max = 65.dp) + ) + } +} + +@Composable +internal fun HorizontalImageTextView( + @DrawableRes drawable: Int, + @StringRes stringRes: Int, + modifier: Modifier = Modifier, textStyle: TextStyle +) { + Row(horizontalArrangement = Arrangement.Start, modifier = modifier) { + Image( + painter = painterResource(id = drawable), + contentDescription = null, + modifier = Modifier.align(Alignment.CenterVertically), + ) + Text( + text = stringResource(id = stringRes), + style = textStyle, + modifier = Modifier + .padding(start = dimensionResource(id = R.dimen.margin_13)) + .align(Alignment.CenterVertically), + color = colorResource(id = R.color.offWhite) + ) + } +} + +@SuppressLint("UnusedMaterialScaffoldPaddingParameter") +@Composable +fun HomeScreen( + channelsViewModel: ChannelsViewModel, + homeClickFunctions: HomeClickFunctions, + balanceTapListener: BalanceTapListener, + tipInterface: FinancialTipClickInterface, + homeViewModel: HomeViewModel +) { + val homeState by homeViewModel.homeState.collectAsState() + val hasNetwork by NetworkMonitor.StateLiveData.get().observeAsState(initial = false) + val simCountryList by channelsViewModel.simCountryList.observeAsState(initial = emptyList()) + val accounts by homeViewModel.accounts.observeAsState(initial = emptyList()) + val context = LocalContext.current + + StaxTheme { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + Scaffold( + topBar = { TopBar(title = R.string.nav_home, isInternetConnected = hasNetwork, homeClickFunctions.onClickedSettingsIcon) }, + content = { + LazyColumn { + if (homeState.bonuses.isNotEmpty() && accounts.isNotEmpty()) + item { + BonusCard(message = homeState.bonuses.first().message, + onClickedTC = homeClickFunctions.onClickedTC, + onClickedTopUp = { + clickedOnBonus( + context, + channelsViewModel, + homeState.bonuses.first() + ) + }) + + } + + if (accounts.isEmpty()) + item { + EmptyBalance(onClickedAddAccount = homeClickFunctions.onClickedAddNewAccount) + } + + item { + PrimaryFeatures( + onSendMoneyClicked = homeClickFunctions.onSendMoneyClicked, + onBuyAirtimeClicked = homeClickFunctions.onBuyAirtimeClicked, + onBuyGoodsClicked = homeClickFunctions.onBuyGoodsClicked, + onPayBillClicked = homeClickFunctions.onPayBillClicked, + onRequestMoneyClicked = homeClickFunctions.onRequestMoneyClicked, + showKEFeatures(simCountryList) + ) + } + + if (accounts.isNotEmpty()) + item { + BalanceHeader( + onClickedAddAccount = homeClickFunctions.onClickedAddNewAccount, homeState.accounts.isNotEmpty() + ) + } + + items(accounts) { account -> + BalanceItem( + staxAccount = account, + context = context, + balanceTapListener = balanceTapListener + ) + } + + item { + homeState.financialTips.firstOrNull { + android.text.format.DateUtils.isToday(it.date!!) + }?.let { + if (homeState.dismissedTipId != it.id) + FinancialTipCard( + tipInterface = tipInterface, + financialTip = homeState.financialTips.first(), + homeViewModel + ) + } + } + } + } + ) + } + } +} + +private fun clickedOnBonus(context: Context, channelsViewModel: ChannelsViewModel, bonus: Bonus) { + AnalyticsUtil.logAnalyticsEvent( + context.getString(R.string.clicked_bonus_airtime_banner), + context + ) + channelsViewModel.validateAccounts(bonus.userChannel) +} + +private fun showKEFeatures(countryIsos: List): Boolean = countryIsos.any { it.contentEquals("KE", ignoreCase = true) } + +@Preview +@Composable +fun HomeScreenPreview() { + val financialTip = FinancialTip( + id = "1234", + title = "Do you want to save money", + content = "This is a test content here so lets see if its going to use ellipse overflow", + snippet = "This is a test content here so lets see if its going to use ellipse overflow, with an example here", + date = System.currentTimeMillis(), + shareCopy = null, + deepLink = null + ) + + StaxTheme { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + Scaffold( + topBar = { + TopBar(title = R.string.nav_home, isInternetConnected = false) {} + }, + content = { padding -> + LazyColumn(modifier = Modifier.padding(padding), content = { + item { + BonusCard(message = "Buy at least Ksh 50 airtime on Stax to get 3% or more bonus airtime", + onClickedTC = {}, + onClickedTopUp = {}) + } + item { + PrimaryFeatures( + onSendMoneyClicked = { }, + onBuyAirtimeClicked = { }, + onBuyGoodsClicked = { }, + onPayBillClicked = { }, + onRequestMoneyClicked = {}, + true + ) + } + item { + BalanceScreenPreview() + } + item { + FinancialTipCard(tipInterface = null, financialTip = financialTip, null) + } + }) + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/presentation/home/HomeState.kt b/app/src/main/java/com/hover/stax/presentation/home/HomeState.kt new file mode 100644 index 000000000..50b1c01ef --- /dev/null +++ b/app/src/main/java/com/hover/stax/presentation/home/HomeState.kt @@ -0,0 +1,12 @@ +package com.hover.stax.presentation.home + +import com.hover.stax.domain.model.Bonus +import com.hover.stax.domain.model.Account +import com.hover.stax.domain.model.FinancialTip + +data class HomeState ( + val bonuses: List = emptyList(), + val accounts: List = emptyList(), + val financialTips: List = emptyList(), + val dismissedTipId: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/presentation/home/HomeViewModel.kt b/app/src/main/java/com/hover/stax/presentation/home/HomeViewModel.kt new file mode 100644 index 000000000..aabb3fde1 --- /dev/null +++ b/app/src/main/java/com/hover/stax/presentation/home/HomeViewModel.kt @@ -0,0 +1,73 @@ +package com.hover.stax.presentation.home + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hover.stax.domain.model.Account +import com.hover.stax.domain.model.Resource +import com.hover.stax.domain.use_case.accounts.GetAccountsUseCase +import com.hover.stax.domain.use_case.bonus.FetchBonusUseCase +import com.hover.stax.domain.use_case.bonus.GetBonusesUseCase +import com.hover.stax.domain.use_case.financial_tips.TipsUseCase +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +class HomeViewModel( + private val getBonusesUseCase: GetBonusesUseCase, + private val fetchBonusUseCase: FetchBonusUseCase, + private val getAccountsUseCase: GetAccountsUseCase, + private val tipsUseCase: TipsUseCase +) : ViewModel() { + + private val _homeState = MutableStateFlow(HomeState()) + val homeState = _homeState.asStateFlow() + + private val _accounts = MutableLiveData>() + val accounts: LiveData> = _accounts + + init { + fetchBonuses() + fetchData() + } + + private fun fetchData() { + getBonusList() + getAccounts() + getFinancialTips() + getDismissedFinancialTips() + } + + private fun fetchBonuses() = viewModelScope.launch { + fetchBonusUseCase() + } + + private fun getBonusList() = viewModelScope.launch { + getBonusesUseCase.bonusList.collect { bonusList -> + _homeState.update { it.copy(bonuses = bonusList) } + } + } + + private fun getAccounts() = viewModelScope.launch { + getAccountsUseCase.accounts.collect { accounts -> + _homeState.update { it.copy(accounts = accounts) } + _accounts.postValue(accounts) + } + } + + private fun getFinancialTips() = tipsUseCase().onEach { result -> + if (result is Resource.Success) + _homeState.update { it.copy(financialTips = result.data ?: emptyList()) } + }.launchIn(viewModelScope) + + private fun getDismissedFinancialTips() = _homeState.update { + it.copy(dismissedTipId = tipsUseCase.getDismissedTipId() ?: "") + } + + fun dismissTip(id: String) { + viewModelScope.launch { + tipsUseCase.dismissTip(id) + _homeState.update { it.copy(dismissedTipId = id) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/requests/NewRequestFragment.kt b/app/src/main/java/com/hover/stax/requests/NewRequestFragment.kt index b0405385c..c0f3ebbb1 100644 --- a/app/src/main/java/com/hover/stax/requests/NewRequestFragment.kt +++ b/app/src/main/java/com/hover/stax/requests/NewRequestFragment.kt @@ -13,15 +13,14 @@ import androidx.annotation.CallSuper import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import com.hover.stax.R -import com.hover.stax.accounts.Account import com.hover.stax.contacts.ContactInput import com.hover.stax.contacts.StaxContact import com.hover.stax.databinding.FragmentRequestBinding +import com.hover.stax.domain.model.Account import com.hover.stax.notifications.PushNotificationTopicsInterface import com.hover.stax.transfers.AbstractFormFragment import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.Utils -import com.hover.stax.utils.collectLatestLifecycleFlow import com.hover.stax.views.* import org.koin.androidx.viewmodel.ext.android.getSharedViewModel import timber.log.Timber @@ -101,14 +100,7 @@ class NewRequestFragment : AbstractFormFragment(), PushNotificationTopicsInterfa } } - with(accountsViewModel) { - collectLatestLifecycleFlow(accounts){ - if (it.isEmpty()) - setDropdownTouchListener(NewRequestFragmentDirections.actionNavigationRequestToAccountsFragment()) - } - - activeAccount.observe(viewLifecycleOwner, accountsObserver) - } + accountsViewModel.activeAccount.observe(viewLifecycleOwner, accountsObserver) with(requestViewModel) { amount.observe(viewLifecycleOwner) { @@ -216,7 +208,7 @@ class NewRequestFragment : AbstractFormFragment(), PushNotificationTopicsInterfa requestViewModel.setEditing(false) } - override fun onSubmitForm() { } + override fun onSubmitForm() {} private fun updatePushNotifGroupStatus() { joinRequestMoneyGroup(requireContext()) diff --git a/app/src/main/java/com/hover/stax/requests/NewRequestViewModel.kt b/app/src/main/java/com/hover/stax/requests/NewRequestViewModel.kt index 62ae02a22..526d96fd3 100644 --- a/app/src/main/java/com/hover/stax/requests/NewRequestViewModel.kt +++ b/app/src/main/java/com/hover/stax/requests/NewRequestViewModel.kt @@ -5,11 +5,11 @@ import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.hover.stax.R -import com.hover.stax.accounts.Account -import com.hover.stax.accounts.AccountRepo -import com.hover.stax.accounts.PLACEHOLDER +import com.hover.stax.domain.model.Account +import com.hover.stax.data.local.accounts.AccountRepo import com.hover.stax.contacts.ContactRepo import com.hover.stax.contacts.StaxContact +import com.hover.stax.domain.model.PLACEHOLDER import com.hover.stax.schedules.ScheduleRepo import com.hover.stax.schedules.Schedule import com.hover.stax.transfers.AbstractFormViewModel @@ -69,7 +69,7 @@ class NewRequestViewModel(application: Application, val repo: RequestRepo, val a fun accountError(): String? = if (activeAccount.value != null) null else getString(R.string.accounts_error_noselect) - fun isValidAccount(): Boolean = activeAccount.value!!.name != PLACEHOLDER + fun isValidAccount(): Boolean = !activeAccount.value!!.name.contains(PLACEHOLDER) fun requesterAcctNoError(): String? = if (!requesterNumber.value.isNullOrEmpty()) null else getString(R.string.requester_number_fielderror) diff --git a/app/src/main/java/com/hover/stax/requests/Request.kt b/app/src/main/java/com/hover/stax/requests/Request.kt index 6f6363b34..6e4738c8e 100644 --- a/app/src/main/java/com/hover/stax/requests/Request.kt +++ b/app/src/main/java/com/hover/stax/requests/Request.kt @@ -8,7 +8,7 @@ import androidx.room.Entity import androidx.room.Ignore import androidx.room.PrimaryKey import com.hover.stax.R -import com.hover.stax.accounts.Account +import com.hover.stax.domain.model.Account import com.hover.stax.channels.Channel import com.hover.stax.contacts.PhoneHelper import com.hover.stax.contacts.StaxContact diff --git a/app/src/main/java/com/hover/stax/requests/RequestDetailFragment.kt b/app/src/main/java/com/hover/stax/requests/RequestDetailFragment.kt index 17b8aca4a..9170db330 100644 --- a/app/src/main/java/com/hover/stax/requests/RequestDetailFragment.kt +++ b/app/src/main/java/com/hover/stax/requests/RequestDetailFragment.kt @@ -13,7 +13,7 @@ import com.hover.stax.contacts.StaxContact import com.hover.stax.databinding.FragmentRequestDetailBinding import com.hover.stax.utils.AnalyticsUtil.logAnalyticsEvent import com.hover.stax.utils.DateUtils -import com.hover.stax.utils.UIHelper.flashMessage +import com.hover.stax.utils.UIHelper.flashAndReportMessage import com.hover.stax.utils.Utils import com.hover.stax.views.Stax2LineItem import com.hover.stax.views.StaxDialog @@ -98,7 +98,7 @@ class RequestDetailFragment: Fragment(), RequestSenderInterface { .setNegButton(R.string.btn_back) {} .setPosButton(R.string.btn_cancelreq) { viewModel.deleteRequest() - flashMessage(requireActivity(), getString(R.string.toast_confirm_cancelreq)) + flashAndReportMessage(requireActivity(), getString(R.string.toast_confirm_cancelreq)) NavHostFragment.findNavController(this@RequestDetailFragment).popBackStack() } .isDestructive diff --git a/app/src/main/java/com/hover/stax/requests/RequestDetailViewModel.kt b/app/src/main/java/com/hover/stax/requests/RequestDetailViewModel.kt index 2ec7c7c1d..431bf1d03 100644 --- a/app/src/main/java/com/hover/stax/requests/RequestDetailViewModel.kt +++ b/app/src/main/java/com/hover/stax/requests/RequestDetailViewModel.kt @@ -1,13 +1,10 @@ package com.hover.stax.requests import androidx.lifecycle.* -import com.hover.stax.accounts.Account -import com.hover.stax.accounts.AccountRepo -import com.hover.stax.channels.Channel -import com.hover.stax.channels.ChannelRepo +import com.hover.stax.domain.model.Account +import com.hover.stax.data.local.accounts.AccountRepo import com.hover.stax.contacts.ContactRepo import com.hover.stax.contacts.StaxContact -import com.hover.stax.schedules.ScheduleRepo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/hover/stax/requests/RequestSenderInterface.kt b/app/src/main/java/com/hover/stax/requests/RequestSenderInterface.kt index a6f5fe25e..9958863f9 100644 --- a/app/src/main/java/com/hover/stax/requests/RequestSenderInterface.kt +++ b/app/src/main/java/com/hover/stax/requests/RequestSenderInterface.kt @@ -11,10 +11,10 @@ import android.view.View import android.widget.TextView import androidx.core.content.ContextCompat import com.hover.stax.R -import com.hover.stax.accounts.Account +import com.hover.stax.domain.model.Account import com.hover.stax.contacts.StaxContact import com.hover.stax.utils.AnalyticsUtil.logAnalyticsEvent -import com.hover.stax.utils.UIHelper.flashMessage +import com.hover.stax.utils.UIHelper.flashAndReportMessage import com.hover.stax.utils.Utils.copyToClipboard const val REQUEST_LINK = "request_link" @@ -112,6 +112,6 @@ interface RequestSenderInterface : SmsSentObserver.SmsSentListener { } fun showError(c: Context) { - flashMessage(c, c.getString(R.string.loading_link_dialoghead)) + flashAndReportMessage(c, c.getString(R.string.loading_link_dialoghead)) } } \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/schedules/ScheduleDetailFragment.kt b/app/src/main/java/com/hover/stax/schedules/ScheduleDetailFragment.kt index deea4ad32..22ec63006 100644 --- a/app/src/main/java/com/hover/stax/schedules/ScheduleDetailFragment.kt +++ b/app/src/main/java/com/hover/stax/schedules/ScheduleDetailFragment.kt @@ -104,7 +104,7 @@ class ScheduleDetailFragment : Fragment() { .setNegButton(R.string.btn_back) {} .setPosButton(R.string.btn_canceltrans) { viewModel.deleteSchedule() - UIHelper.flashMessage(requireActivity(), getString(R.string.toast_confirm_cancelfuture)) + UIHelper.flashAndReportMessage(requireActivity(), getString(R.string.toast_confirm_cancelfuture)) findNavController().popBackStack() } .isDestructive @@ -119,7 +119,7 @@ class ScheduleDetailFragment : Fragment() { ScheduleWorker.makeWork()).enqueue() if (!schedule.isScheduledForToday) - UIHelper.flashMessage(requireActivity(), "Shouldn't show notification; not scheduled for today") + UIHelper.flashAndReportMessage(requireActivity(), "Shouldn't show notification; not scheduled for today") } } } diff --git a/app/src/main/java/com/hover/stax/schedules/ScheduleDetailViewModel.kt b/app/src/main/java/com/hover/stax/schedules/ScheduleDetailViewModel.kt index e2e8a207e..092f05dbc 100644 --- a/app/src/main/java/com/hover/stax/schedules/ScheduleDetailViewModel.kt +++ b/app/src/main/java/com/hover/stax/schedules/ScheduleDetailViewModel.kt @@ -2,7 +2,7 @@ package com.hover.stax.schedules import androidx.lifecycle.* import com.hover.sdk.actions.HoverAction -import com.hover.stax.actions.ActionRepo +import com.hover.stax.data.local.actions.ActionRepo import com.hover.stax.contacts.ContactRepo import com.hover.stax.contacts.StaxContact import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/hover/stax/settings/SettingsFragment.kt b/app/src/main/java/com/hover/stax/settings/SettingsFragment.kt index a205796b0..7391735c2 100644 --- a/app/src/main/java/com/hover/stax/settings/SettingsFragment.kt +++ b/app/src/main/java/com/hover/stax/settings/SettingsFragment.kt @@ -16,7 +16,7 @@ import androidx.navigation.fragment.findNavController import com.hover.sdk.api.Hover import com.hover.stax.BuildConfig import com.hover.stax.R -import com.hover.stax.accounts.Account +import com.hover.stax.domain.model.Account import com.hover.stax.accounts.AccountsViewModel import com.hover.stax.databinding.FragmentSettingsBinding import com.hover.stax.languages.LanguageViewModel @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.getViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import timber.log.Timber const val TEST_MODE = "test_mode" @@ -68,10 +69,8 @@ class SettingsFragment : Fragment() { binding.bountyCard.getStartedWithBountyButton.setOnClickListener { startBounties() } - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - accountsViewModel.accountUpdateMsg.collect { UIHelper.flashMessage(requireActivity(), it) } - } + collectLifecycleFlow(accountsViewModel.accountUpdateMsg) { + UIHelper.flashAndReportMessage(requireActivity(), it) } } @@ -91,12 +90,12 @@ class SettingsFragment : Fragment() { NavUtil.navigate(findNavController(), SettingsFragmentDirections.actionNavigationSettingsToNavigationLinkAccount()) } - collectLatestLifecycleFlow(accountsViewModel.accounts) { - if (it.isEmpty()) { + collectLifecycleFlow(accountsViewModel.accountList) { + if (it.accounts.isEmpty()) { binding.settingsCard.defaultAccountEntry.visibility = GONE binding.settingsCard.connectAccounts.visibility = VISIBLE } else - createDefaultSelector(it) + createDefaultSelector(it.accounts) } } @@ -166,6 +165,7 @@ class SettingsFragment : Fragment() { } private fun createDefaultSelector(accounts: List) { + binding.settingsCard.connectAccounts.visibility = GONE val spinner = binding.settingsCard.defaultAccountSpinner binding.settingsCard.defaultAccountEntry.visibility = VISIBLE accountAdapter = ArrayAdapter(requireActivity(), R.layout.stax_spinner_item, accounts) @@ -187,23 +187,24 @@ class SettingsFragment : Fragment() { private fun setUpEnableTestMode() { binding.settingsCard.testMode.setOnCheckedChangeListener { _, isChecked -> Utils.saveBoolean(TEST_MODE, isChecked, requireContext()) - UIHelper.flashMessage(requireContext(), if (isChecked) R.string.test_mode_toast else R.string.test_mode_disabled) + UIHelper.flashAndReportMessage(requireContext(), if (isChecked) R.string.test_mode_toast else R.string.test_mode_disabled) } binding.settingsCard.testMode.visibility = if (Utils.getBoolean(TEST_MODE, requireContext())) VISIBLE else GONE binding.disclaimer.setOnClickListener { clickCounter++ - if (clickCounter == 5) UIHelper.flashMessage(requireContext(), R.string.test_mode_almost_toast) else if (clickCounter == 7) enableTestMode() + if (clickCounter == 5) UIHelper.flashAndReportMessage(requireContext(), R.string.test_mode_almost_toast) else if (clickCounter == 7) enableTestMode() } } private fun enableTestMode() { Utils.saveBoolean(TEST_MODE, true, requireActivity()) binding.settingsCard.testMode.visibility = VISIBLE - UIHelper.flashMessage(requireContext(), R.string.test_mode_toast) + UIHelper.flashAndReportMessage(requireContext(), R.string.test_mode_toast) } private fun startBounties() { val staxUser = loginViewModel.staxUser.value + val navDirection = if (staxUser == null || !staxUser.isMapper) SettingsFragmentDirections.actionNavigationSettingsToBountyEmailFragment() else @@ -224,7 +225,7 @@ class SettingsFragment : Fragment() { private fun logoutUser() { loginViewModel.silentSignOut() binding.staxSupport.marketingOptIn.isChecked = false - UIHelper.flashMessage(requireActivity(), getString(R.string.logout_out_success)) + UIHelper.flashAndReportMessage(requireActivity(), getString(R.string.logout_out_success)) } private fun showLoginDialog() { diff --git a/app/src/main/java/com/hover/stax/transactionDetails/TransactionDetailsFragment.kt b/app/src/main/java/com/hover/stax/transactionDetails/TransactionDetailsFragment.kt index fbdb4f2f0..ccfc01af2 100644 --- a/app/src/main/java/com/hover/stax/transactionDetails/TransactionDetailsFragment.kt +++ b/app/src/main/java/com/hover/stax/transactionDetails/TransactionDetailsFragment.kt @@ -9,6 +9,7 @@ import android.view.ViewGroup import android.view.animation.AnimationUtils import android.widget.RelativeLayout import android.widget.TextView +import androidx.core.text.HtmlCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer @@ -20,9 +21,9 @@ import com.hover.sdk.api.Hover import com.hover.sdk.transactions.Transaction import com.hover.stax.ApplicationInstance import com.hover.stax.R -import com.hover.stax.accounts.Account import com.hover.stax.contacts.StaxContact import com.hover.stax.databinding.FragmentTransactionBinding +import com.hover.stax.domain.model.Account import com.hover.stax.home.MainActivity import com.hover.stax.hover.AbstractHoverCallerActivity import com.hover.stax.merchants.Merchant @@ -87,18 +88,28 @@ class TransactionDetailsFragment : Fragment() { } private fun startObservers() = with(viewModel) { + val txnObserver = object : Observer { + override fun onChanged(t: Transaction?) { + t?.let { Timber.e("Updating transaction messages ${t.uuid}") } + } + } + transaction.observe(viewLifecycleOwner) { showTransaction(it) } action.observe(viewLifecycleOwner) { it?.let { updateAction(it) } } contact.observe(viewLifecycleOwner) { updateRecipient(it) } merchant.observe(viewLifecycleOwner) { updateRecipient(it) } account.observe(viewLifecycleOwner) { it?.let { updateAccount(it) } } - hoverTransaction.observe(viewLifecycleOwner) { it?.let { Timber.e("Updating transaction messages ${it.uuid}") } } + hoverTransaction.observe(viewLifecycleOwner, txnObserver) messages.observe(viewLifecycleOwner) { it?.let { updateMessages(it) } } bonusAmt.observe(viewLifecycleOwner) { showBonusAmount(it) } - val observer = Observer { - Timber.i("Expecting sms $it") - action.value?.let { a -> updateAction(a) } + + val observer = object: Observer { + override fun onChanged(t: Boolean?) { + Timber.i("Expecting sms $t") + action.value?.let { a -> updateAction(a) } + } + } isExpectingSMS.observe(viewLifecycleOwner, observer) } @@ -134,7 +145,8 @@ class TransactionDetailsFragment : Fragment() { detailsDate.text = humanFriendlyDateTime(transaction.updated_at) typeValue.text = transaction.toString(requireContext()) viewModel.action.value?.let { - categoryValue.text = transaction.shortStatusExplain(viewModel.action.value, requireContext()) } + categoryValue.text = transaction.shortStatusExplain(viewModel.action.value, requireContext()) + } statusValue.apply { text = transaction.humanStatus(requireContext()) @@ -171,7 +183,8 @@ class TransactionDetailsFragment : Fragment() { if (action.isOnNetwork) binding.details.recipInstitutionRow.visibility = GONE else binding.details.institutionValue.setTitle(action.to_institution_name) viewModel.transaction.value?.let { - binding.statusInfo.longDescription.text = it.longStatus(action, viewModel.messages.value?.last(), viewModel.sms.value, viewModel.isExpectingSMS.value ?: false, requireContext()) + val msg = it.longStatus(action, viewModel.messages.value?.last(), viewModel.sms.value, viewModel.isExpectingSMS.value ?: false, requireContext()) + binding.statusInfo.longDescription.text = HtmlCompat.fromHtml(msg, HtmlCompat.FROM_HTML_MODE_LEGACY) binding.details.categoryValue.text = it.shortStatusExplain(action, requireContext()) if (action.transaction_type == HoverAction.BILL) binding.details.institutionValue.setSubtitle(Paybill.extractBizNumber(action)) @@ -187,13 +200,14 @@ class TransactionDetailsFragment : Fragment() { private fun updateMessages(ussdCallResponses: List?) { viewModel.action.value?.let { viewModel.transaction.value?.let { t -> - binding.statusInfo.longDescription.text = t.longStatus( + val msg = t.longStatus( it, ussdCallResponses?.last(), viewModel.sms.value, viewModel.isExpectingSMS.value ?: false, requireContext() ) + binding.statusInfo.longDescription.text = HtmlCompat.fromHtml(msg, HtmlCompat.FROM_HTML_MODE_LEGACY) } } } @@ -218,13 +232,13 @@ class TransactionDetailsFragment : Fragment() { private fun addRetryOrSupportButton(transaction: StaxTransaction) { if (transaction.isRecorded) - binding.statusInfo.btnRetry.setOnClickListener{ retryBounty() } + binding.statusInfo.btnRetry.setOnClickListener { retryBounty() } else if (transaction.status == Transaction.FAILED) { if (shouldContactSupport(transaction.action_id)) setupContactSupportButton(transaction.action_id, binding.statusInfo.btnRetry) - else binding.statusInfo.btnRetry.setOnClickListener{ maybeRetry(transaction) } + else binding.statusInfo.btnRetry.setOnClickListener { maybeRetry(transaction) } } - binding.statusInfo.btnRetry.visibility = if (transaction.isRetryable) VISIBLE else GONE + binding.statusInfo.btnRetry.visibility = if (transaction.canRetry) VISIBLE else GONE } private fun shouldContactSupport(id: String): Boolean = if (retryCounter[id] != null) retryCounter[id]!! >= 3 else false @@ -241,7 +255,7 @@ class TransactionDetailsFragment : Fragment() { private fun maybeRetry(transaction: StaxTransaction) { if (viewModel.account.value == null || viewModel.action.value == null || viewModel.transaction.value == null) - UIHelper.flashMessage(requireContext(), getString(R.string.error_still_loading)) + UIHelper.flashAndReportError(requireContext(), R.string.error_still_loading) else { retry(transaction) } @@ -285,7 +299,7 @@ class TransactionDetailsFragment : Fragment() { private fun showShareExcitement(transaction: StaxTransaction) { val isTransactionSuccessful = !transaction.isRecorded && transaction.isSuccessful - val shareMessage = when(transaction.transaction_type) { + val shareMessage = when (transaction.transaction_type) { HoverAction.AIRTIME -> getString(R.string.airtime_purchase_message, getString(R.string.share_link)) HoverAction.BALANCE -> getString(R.string.check_balance_message, getString(R.string.share_link)) HoverAction.P2P -> getString(R.string.send_money_message, getString(R.string.share_link)) @@ -300,7 +314,7 @@ class TransactionDetailsFragment : Fragment() { private fun setBottomSheetVisibility(isVisible: Boolean, shareMessage: String) { var updatedState = BottomSheetBehavior.STATE_HIDDEN - if(isVisible) { + if (isVisible) { updatedState = BottomSheetBehavior.STATE_EXPANDED val animation = AnimationUtils.loadAnimation(requireContext(), R.anim.slide_down) @@ -316,7 +330,7 @@ class TransactionDetailsFragment : Fragment() { private fun showBonusAmount(amount: Int) = with(binding.details) { val txn = viewModel.transaction.value - if(amount > 0 && (txn != null && txn.isSuccessful)){ + if (amount > 0 && (txn != null && txn.isSuccessful)) { bonusRow.visibility = VISIBLE bonusAmount.text = amount.toString() } else { diff --git a/app/src/main/java/com/hover/stax/transactionDetails/TransactionDetailsViewModel.kt b/app/src/main/java/com/hover/stax/transactionDetails/TransactionDetailsViewModel.kt index a80628085..fceddcf3c 100644 --- a/app/src/main/java/com/hover/stax/transactionDetails/TransactionDetailsViewModel.kt +++ b/app/src/main/java/com/hover/stax/transactionDetails/TransactionDetailsViewModel.kt @@ -6,19 +6,18 @@ import com.hover.sdk.actions.HoverAction import com.hover.sdk.api.Hover import com.hover.sdk.api.Hover.getSMSMessageByUUID import com.hover.sdk.transactions.Transaction -import com.hover.stax.accounts.Account -import com.hover.stax.accounts.AccountRepo -import com.hover.stax.actions.ActionRepo -import com.hover.stax.bonus.BonusRepo +import com.hover.stax.domain.model.Account +import com.hover.stax.data.local.accounts.AccountRepo +import com.hover.stax.data.local.actions.ActionRepo +import com.hover.stax.data.local.bonus.BonusRepo import com.hover.stax.contacts.ContactRepo import com.hover.stax.contacts.StaxContact -import com.hover.stax.database.ParserRepo +import com.hover.stax.data.local.parser.ParserRepo import com.hover.stax.merchants.Merchant import com.hover.stax.merchants.MerchantRepo import com.hover.stax.transactions.StaxTransaction import com.hover.stax.transactions.TransactionRepo import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.json.JSONArray import timber.log.Timber diff --git a/app/src/main/java/com/hover/stax/transactions/StaxTransaction.kt b/app/src/main/java/com/hover/stax/transactions/StaxTransaction.kt index eda249b7f..00da4e7c1 100644 --- a/app/src/main/java/com/hover/stax/transactions/StaxTransaction.kt +++ b/app/src/main/java/com/hover/stax/transactions/StaxTransaction.kt @@ -15,7 +15,7 @@ import timber.log.Timber import com.hover.stax.R import com.hover.sdk.api.HoverParameters import com.hover.sdk.transactions.Transaction -import com.hover.stax.accounts.ACCOUNT_ID +import com.hover.stax.domain.model.ACCOUNT_ID import com.hover.stax.paybill.BUSINESS_NO import com.hover.stax.utils.Utils import java.util.HashMap @@ -106,13 +106,12 @@ data class StaxTransaction( } private fun getCounterPartyNo(intent: Intent, contact: StaxContact?): String? { - if (contact != null) - return contact.accountNumber - else if (intent.hasExtra(HoverAction.ACCOUNT_KEY)) - return intent.getStringExtra(HoverAction.ACCOUNT_KEY) - else if (intent.hasExtra(BUSINESS_NO)) - return intent.getStringExtra(BUSINESS_NO) - else return null + return when { + contact != null -> contact.accountNumber + intent.hasExtra(HoverAction.ACCOUNT_KEY) -> intent.getStringExtra(HoverAction.ACCOUNT_KEY) + intent.hasExtra(BUSINESS_NO) -> intent.getStringExtra(BUSINESS_NO) + else -> null + } } fun update(data: Intent, action: HoverAction, contact: StaxContact, context: Context) { @@ -154,7 +153,7 @@ data class StaxTransaction( } } - val isRetryable: Boolean + val canRetry: Boolean get() = isRecorded || ((transaction_type == HoverAction.P2P || transaction_type == HoverAction.AIRTIME || transaction_type == HoverAction.BALANCE) && isFailed) diff --git a/app/src/main/java/com/hover/stax/transactions/TransactionDao.kt b/app/src/main/java/com/hover/stax/transactions/TransactionDao.kt index 289441b54..fe08e33e6 100644 --- a/app/src/main/java/com/hover/stax/transactions/TransactionDao.kt +++ b/app/src/main/java/com/hover/stax/transactions/TransactionDao.kt @@ -7,6 +7,7 @@ import com.hover.sdk.transactions.Transaction as Txn @Dao interface TransactionDao { + @Query("SELECT * FROM stax_transactions WHERE channel_id = :channelId AND transaction_type != 'balance' AND status != 'failed' AND environment != 3 ORDER BY initiated_at DESC") fun getCompleteAndPendingTransfers(channelId: Int): LiveData>? @@ -23,6 +24,9 @@ interface TransactionDao { @get:Query("SELECT * FROM stax_transactions WHERE environment = 3 ORDER BY initiated_at DESC") val bountyTransactions: LiveData>? + @get:Query("SELECT * FROM stax_transactions WHERE environment = 3 ORDER BY initiated_at DESC") + val bountyTransactionList: List + @get:Query("SELECT * FROM stax_transactions WHERE environment != 3 AND account_id IS NOT NULL ORDER BY initiated_at DESC") val nonBountyTransactions: LiveData> @@ -49,4 +53,8 @@ interface TransactionDao { @Update fun update(transaction: StaxTransaction?) + + @Query("DELETE FROM stax_transactions WHERE account_id = :accountId") + fun deleteAccountTransactions(accountId: Int) + } \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/transactions/TransactionHistoryFragment.kt b/app/src/main/java/com/hover/stax/transactions/TransactionHistoryFragment.kt index 4a5a7ddf0..fb7a40d9d 100644 --- a/app/src/main/java/com/hover/stax/transactions/TransactionHistoryFragment.kt +++ b/app/src/main/java/com/hover/stax/transactions/TransactionHistoryFragment.kt @@ -4,57 +4,83 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.hover.stax.R import com.hover.stax.databinding.TransactionCardHistoryBinding +import com.hover.stax.presentation.home.TopBar +import com.hover.stax.ui.theme.StaxTheme import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.NavUtil import com.hover.stax.utils.UIHelper +import com.hover.stax.utils.network.NetworkMonitor import org.koin.androidx.viewmodel.ext.android.viewModel class TransactionHistoryFragment : Fragment(), TransactionHistoryAdapter.SelectListener { - private var _binding: TransactionCardHistoryBinding? = null - private val binding get() = _binding!! - - private val viewModel: TransactionHistoryViewModel by viewModel() - private var transactionsAdapter: TransactionHistoryAdapter? = null - - override fun onCreateView(inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?): View { - AnalyticsUtil.logAnalyticsEvent(getString(R.string.visit_screen, getString(R.string.visit_transaction_history)), requireActivity()) - _binding = TransactionCardHistoryBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initRecyclerView() - observeTransactionActionPair() - } - - private fun initRecyclerView() { - binding.transactionsRecycler.apply { - layoutManager = UIHelper.setMainLinearManagers(context) - transactionsAdapter = TransactionHistoryAdapter(this@TransactionHistoryFragment) - adapter = transactionsAdapter - } - } - - private fun observeTransactionActionPair() { - viewModel.transactionHistory.observe(viewLifecycleOwner) { - binding.noHistory.visibility = if (it.isNullOrEmpty()) View.VISIBLE else View.GONE - transactionsAdapter!!.submitList(it) - } - } - - override fun viewTransactionDetail(uuid: String?) { - uuid?.let { NavUtil.showTransactionDetailsFragment(findNavController(), it) } - } - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } + private var _binding: TransactionCardHistoryBinding? = null + private val binding get() = _binding!! + + private val viewModel: TransactionHistoryViewModel by viewModel() + private var transactionsAdapter: TransactionHistoryAdapter? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + AnalyticsUtil.logAnalyticsEvent(getString(R.string.visit_screen, getString(R.string.visit_transaction_history)), requireActivity()) + _binding = TransactionCardHistoryBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initToolbar() + + initRecyclerView() + observeTransactionActionPair() + } + + private fun initToolbar() { + binding.toolbar.setContent { + StaxTheme { Toolbar() } + } + } + + @Composable + private fun Toolbar() { + val hasNetwork by NetworkMonitor.StateLiveData.get().observeAsState(initial = false) + TopBar(title = R.string.nav_history, isInternetConnected = hasNetwork) { + findNavController().navigate(TransactionHistoryFragmentDirections.actionGlobalNavigationSettings()) + } + } + + private fun initRecyclerView() { + binding.transactionsRecycler.apply { + layoutManager = UIHelper.setMainLinearManagers(context) + transactionsAdapter = TransactionHistoryAdapter(this@TransactionHistoryFragment) + adapter = transactionsAdapter + } + } + + private fun observeTransactionActionPair() { + viewModel.transactionHistory.observe(viewLifecycleOwner) { + binding.noHistory.visibility = if (it.isNullOrEmpty()) View.VISIBLE else View.GONE + transactionsAdapter!!.submitList(it) + } + } + + override fun viewTransactionDetail(uuid: String?) { + uuid?.let { NavUtil.showTransactionDetailsFragment(findNavController(), it) } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/transactions/TransactionHistoryViewModel.kt b/app/src/main/java/com/hover/stax/transactions/TransactionHistoryViewModel.kt index 89c632d20..bb825c3f2 100644 --- a/app/src/main/java/com/hover/stax/transactions/TransactionHistoryViewModel.kt +++ b/app/src/main/java/com/hover/stax/transactions/TransactionHistoryViewModel.kt @@ -2,19 +2,21 @@ package com.hover.stax.transactions import androidx.lifecycle.* import com.hover.sdk.actions.HoverAction -import com.hover.stax.actions.ActionRepo +import com.hover.stax.data.local.actions.ActionRepo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class TransactionHistoryViewModel(val repo: TransactionRepo, val actionRepo: ActionRepo) : ViewModel() { - private val allNonBountyTransaction : LiveData> = repo.allNonBountyTransactions - var transactionHistory : MediatorLiveData> = MediatorLiveData() + private val allNonBountyTransaction: LiveData> = repo.allNonBountyTransactions + var transactionHistory: MediatorLiveData> = MediatorLiveData() private var staxTransactions: LiveData> = MutableLiveData() private val appReviewLiveData: LiveData init { transactionHistory.addSource(allNonBountyTransaction, this::getTransactionHistory) + staxTransactions = repo.completeAndPendingTransferTransactions!! + appReviewLiveData = Transformations.map(repo.transactionsForAppReview!!) { showAppReview(it) } } private fun getTransactionHistory(transactions: List) { @@ -40,11 +42,6 @@ class TransactionHistoryViewModel(val repo: TransactionRepo, val actionRepo: Act } return if (balancesTransactions >= 4) true else transfersAndAirtime >= 2 } - - init { - staxTransactions = repo.completeAndPendingTransferTransactions!! - appReviewLiveData = Transformations.map(repo.transactionsForAppReview!!) { showAppReview(it) } - } } - data class TransactionHistory(val staxTransaction: StaxTransaction, val action: HoverAction?) \ No newline at end of file +data class TransactionHistory(val staxTransaction: StaxTransaction, val action: HoverAction?) \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/transactions/TransactionRepo.kt b/app/src/main/java/com/hover/stax/transactions/TransactionRepo.kt index d636ba2c7..87a159c2a 100644 --- a/app/src/main/java/com/hover/stax/transactions/TransactionRepo.kt +++ b/app/src/main/java/com/hover/stax/transactions/TransactionRepo.kt @@ -5,12 +5,11 @@ import android.content.Context import android.content.Intent import androidx.lifecycle.LiveData import com.hover.sdk.actions.HoverAction -import com.hover.sdk.database.HoverRoomDatabase import com.hover.sdk.transactions.TransactionContract import com.hover.stax.R -import com.hover.stax.accounts.Account import com.hover.stax.contacts.StaxContact import com.hover.stax.database.AppDatabase +import com.hover.stax.domain.model.Account import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.DateUtils import kotlinx.coroutines.flow.Flow @@ -28,9 +27,12 @@ class TransactionRepo(db: AppDatabase) { val transactionsForAppReview: LiveData>? get() = transactionDao.transactionsForAppReview - val allNonBountyTransactions : LiveData> + val allNonBountyTransactions: LiveData> get() = transactionDao.nonBountyTransactions + val bountyTransactionList: List + get() = transactionDao.bountyTransactionList + @SuppressLint("DefaultLocale") suspend fun hasTransactionLastMonth(): Boolean { return transactionDao.getTransactionCount(String.format("%02d", DateUtils.lastMonth().first), DateUtils.lastMonth().second.toString())!! > 0 @@ -56,6 +58,8 @@ class TransactionRepo(db: AppDatabase) { fun getTransactionAsync(uuid: String): Flow = transactionDao.getTransactionAsync(uuid) + fun deleteAccountTransactions(accountId: Int) = transactionDao.deleteAccountTransactions(accountId) + fun insertOrUpdateTransaction(intent: Intent, action: HoverAction, contact: StaxContact, c: Context) { AppDatabase.databaseWriteExecutor.execute { try { diff --git a/app/src/main/java/com/hover/stax/transfers/AbstractFormFragment.kt b/app/src/main/java/com/hover/stax/transfers/AbstractFormFragment.kt index cd6bafabb..7672fb77f 100644 --- a/app/src/main/java/com/hover/stax/transfers/AbstractFormFragment.kt +++ b/app/src/main/java/com/hover/stax/transfers/AbstractFormFragment.kt @@ -17,18 +17,18 @@ import androidx.navigation.NavDirections import androidx.navigation.fragment.findNavController import com.hover.sdk.actions.HoverAction import com.hover.stax.R -import com.hover.stax.accounts.Account import com.hover.stax.accounts.AccountDropdown import com.hover.stax.accounts.AccountsViewModel import com.hover.stax.actions.ActionSelectViewModel -import com.hover.stax.balances.BalancesViewModel import com.hover.stax.contacts.StaxContact +import com.hover.stax.domain.model.Account import com.hover.stax.hover.AbstractHoverCallerActivity import com.hover.stax.permissions.PermissionUtils +import com.hover.stax.presentation.home.BalancesViewModel import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.NavUtil import com.hover.stax.utils.UIHelper -import com.hover.stax.utils.collectLatestLifecycleFlow +import com.hover.stax.utils.collectLifecycleFlow import com.hover.stax.views.AbstractStatefulInput import com.hover.stax.views.StaxCardView import com.hover.stax.views.StaxDialog @@ -76,7 +76,7 @@ abstract class AbstractFormFragment : Fragment() { payWithDropdown.setObservers(accountsViewModel, viewLifecycleOwner) abstractFormViewModel.isEditing.observe(viewLifecycleOwner, Observer(this::showEdit)) - collectLatestLifecycleFlow(balancesViewModel.balanceAction) { + collectLifecycleFlow(balancesViewModel.balanceAction) { callHover(accountsViewModel.activeAccount.value, it) } } @@ -104,7 +104,7 @@ abstract class AbstractFormFragment : Fragment() { } else { onSubmitForm() } - } else UIHelper.flashMessage(requireActivity(), getString(R.string.toast_pleasefix)) + } else UIHelper.flashAndReportMessage(requireActivity(), getString(R.string.toast_pleasefix)) } abstract fun validates(): Boolean @@ -174,7 +174,7 @@ abstract class AbstractFormFragment : Fragment() { private fun showError(userMsg: Int, logMsg: Int) { log(getString(logMsg)) - UIHelper.flashMessage(requireContext(), getString(userMsg)) + UIHelper.flashAndReportMessage(requireContext(), getString(userMsg)) } abstract fun onContactSelected(contact: StaxContact) @@ -195,7 +195,7 @@ abstract class AbstractFormFragment : Fragment() { private val backPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - Timber.e("Caught back press. isediting: %s", abstractFormViewModel.isEditing.value) + Timber.e("Caught back press. is editing: %s", abstractFormViewModel.isEditing.value) if (abstractFormViewModel.isEditing.value == false) abstractFormViewModel.setEditing(true) else diff --git a/app/src/main/java/com/hover/stax/transfers/AbstractFormViewModel.kt b/app/src/main/java/com/hover/stax/transfers/AbstractFormViewModel.kt index 41aae8159..6ba1e3a45 100644 --- a/app/src/main/java/com/hover/stax/transfers/AbstractFormViewModel.kt +++ b/app/src/main/java/com/hover/stax/transfers/AbstractFormViewModel.kt @@ -15,7 +15,7 @@ import com.hover.stax.schedules.ScheduleRepo import com.hover.stax.schedules.Schedule import com.hover.stax.utils.AnalyticsUtil -abstract class AbstractFormViewModel(application: Application, val contactRepo: ContactRepo, val scheduleRepo: ScheduleRepo) : AndroidViewModel(application) { +abstract class AbstractFormViewModel(application: Application, val contactRepo: ContactRepo, private val scheduleRepo: ScheduleRepo) : AndroidViewModel(application) { var recentContacts: LiveData> = MutableLiveData() val schedule = MutableLiveData() diff --git a/app/src/main/java/com/hover/stax/transfers/TransferFragment.kt b/app/src/main/java/com/hover/stax/transfers/TransferFragment.kt index cce5e9d89..e14215803 100644 --- a/app/src/main/java/com/hover/stax/transfers/TransferFragment.kt +++ b/app/src/main/java/com/hover/stax/transfers/TransferFragment.kt @@ -10,7 +10,6 @@ import android.view.ViewGroup import android.widget.LinearLayout import androidx.annotation.CallSuper import androidx.core.content.ContextCompat.getColor -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.hover.sdk.actions.HoverAction @@ -25,9 +24,8 @@ import com.hover.stax.hover.FEE_REQUEST import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.UIHelper import com.hover.stax.utils.Utils -import com.hover.stax.utils.collectLatestLifecycleFlow +import com.hover.stax.utils.collectLifecycleFlow import com.hover.stax.views.AbstractStatefulInput -import kotlinx.coroutines.flow.collect import org.koin.androidx.viewmodel.ext.android.getSharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.viewModel @@ -48,6 +46,8 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, private var nonStandardVariableAdapter: NonStandardVariableAdapter? = null private lateinit var nonStandardSummaryAdapter: NonStandardSummaryAdapter + private var hasBonus = false + @CallSuper override fun onCreate(savedInstanceState: Bundle?) { abstractFormViewModel = getSharedViewModel() @@ -120,7 +120,7 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, override fun startObservers(root: View) { super.startObservers(root) - observeAccountList() + observeActiveAccount() observeActions() observeActionSelection() @@ -153,7 +153,7 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, private fun observeActions() { accountsViewModel.channelActions.observe(viewLifecycleOwner) { actionSelectViewModel.setActions(it) - showBonusBanner(it.firstOrNull()) + showBonusBanner(it) } actionSelectViewModel.filteredActions.observe(viewLifecycleOwner) { @@ -169,11 +169,6 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, } } - private fun observeAccountList() = collectLatestLifecycleFlow(accountsViewModel.accounts) { - if (it.isEmpty()) - setDropdownTouchListener(TransferFragmentDirections.actionNavigationTransferToAccountsFragment()) - } - private fun observeAmount() { transferViewModel.amount.observe(viewLifecycleOwner) { it?.let { @@ -212,11 +207,9 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, } private fun observeAccountsEvent() { - lifecycleScope.launchWhenStarted { - channelsViewModel.accountEventFlow.collect { - val bonus = bonusViewModel.bonusList.value.bonuses.firstOrNull() ?: return@collect - accountsViewModel.setActiveAccountFromChannel(bonus.userChannel) - } + collectLifecycleFlow(channelsViewModel.accountEventFlow) { + val bonus = bonusViewModel.bonusList.value.bonuses.firstOrNull() ?: return@collectLifecycleFlow + accountsViewModel.setActiveAccountFromChannel(bonus.userChannel) } } @@ -265,7 +258,7 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, } private fun getExtras(): HashMap { - val extras = transferViewModel.wrapExtras() + val extras = transferViewModel.wrapExtras(hasBonus) extras.putAll(actionSelectViewModel.wrapExtras()) return extras } @@ -344,7 +337,7 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, } } - private fun showBonusBanner(activeAction: HoverAction?) { + private fun showBonusBanner(actions: List) { if (args.transactionType == HoverAction.AIRTIME) { val bonus = bonusViewModel.bonusList.value.bonuses.firstOrNull() ?: return @@ -352,7 +345,9 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, cardBonus.visibility = View.VISIBLE learnMore.movementMethod = LinkMovementMethod.getInstance() - if (activeAction?.channel_id == bonus.purchaseChannel) { + hasBonus = actions.map { it.channel_id }.contains(bonus.userChannel) + + if (hasBonus) { title.text = getString(R.string.congratulations) message.text = getString(R.string.valid_account_bonus_msg) cta.visibility = View.GONE @@ -379,7 +374,7 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, if (!isEditing) binding.bonusLayout.cardBonus.visibility = View.GONE else - showBonusBanner(actionSelectViewModel.activeAction.value) + showBonusBanner(actionSelectViewModel.filteredActions.value ?: emptyList()) } override fun onDestroyView() { diff --git a/app/src/main/java/com/hover/stax/transfers/TransferViewModel.kt b/app/src/main/java/com/hover/stax/transfers/TransferViewModel.kt index 85a6cbf1c..8f7a8c0f4 100644 --- a/app/src/main/java/com/hover/stax/transfers/TransferViewModel.kt +++ b/app/src/main/java/com/hover/stax/transfers/TransferViewModel.kt @@ -1,7 +1,6 @@ package com.hover.stax.transfers import android.app.Application -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.hover.sdk.actions.HoverAction @@ -9,16 +8,20 @@ import com.hover.stax.R import com.hover.stax.contacts.ContactRepo import com.hover.stax.contacts.PhoneHelper import com.hover.stax.contacts.StaxContact -import com.hover.stax.schedules.ScheduleRepo import com.hover.stax.requests.Request import com.hover.stax.requests.RequestRepo +import com.hover.stax.schedules.ScheduleRepo import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.DateUtils +import com.hover.stax.utils.Utils import com.yariksoffice.lingver.Lingver import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import timber.log.Timber +const val STAX_PREFIX = "stax_airtime_prefix" +private const val KE_PREFIX = "0" + class TransferViewModel(application: Application, private val requestRepo: RequestRepo, contactRepo: ContactRepo, scheduleRepo: ScheduleRepo) : AbstractFormViewModel(application, contactRepo, scheduleRepo) { val amount = MutableLiveData() @@ -48,16 +51,18 @@ class TransferViewModel(application: Application, private val requestRepo: Reque try { val formattedPhone = PhoneHelper.getNationalSignificantNumber( it.requester_number!!, - countryAlpha2 ?: Lingver.getInstance().getLocale().country) + countryAlpha2 ?: Lingver.getInstance().getLocale().country + ) val sc = contactRepo.getContactByPhone(formattedPhone) - contact.postValue( sc ?: StaxContact(r.requester_number)) + contact.postValue(sc ?: StaxContact(r.requester_number)) isLoading.postValue(false) } catch (e: NumberFormatException) { AnalyticsUtil.logErrorAndReportToFirebase( - TransferViewModel::class.java.simpleName, e.message!!, e) + TransferViewModel::class.java.simpleName, e.message!!, e + ) } } - } + } private fun setNote(n: String?) = note.postValue(n) @@ -73,18 +78,21 @@ class TransferViewModel(application: Application, private val requestRepo: Reque } } - fun wrapExtras(): HashMap { + fun wrapExtras(isBonusAirtime: Boolean = false): HashMap { val extras: HashMap = hashMapOf() if (amount.value != null) extras[HoverAction.AMOUNT_KEY] = amount.value!! if (contact.value != null && contact.value?.accountNumber != null) { extras[StaxContact.ID_KEY] = contact.value!!.id extras[HoverAction.PHONE_KEY] = contact.value!!.accountNumber - extras[HoverAction.ACCOUNT_KEY] = contact.value!!.accountNumber + extras[HoverAction.ACCOUNT_KEY] = if (isBonusAirtime) staxPrefix.plus(KE_PREFIX).plus(PhoneHelper.getNationalSignificantNumber(contact.value!!.accountNumber, "KE")) else + contact.value!!.accountNumber } if (note.value != null) extras[HoverAction.NOTE_KEY] = note.value!! return extras } + private val staxPrefix get() = Utils.getString(STAX_PREFIX, getApplication()) + fun load(encryptedString: String) = viewModelScope.launch { isLoading.postValue(true) val r: Request? = requestRepo.decrypt(encryptedString, getApplication()) diff --git a/app/src/main/java/com/hover/stax/ui/theme/Color.kt b/app/src/main/java/com/hover/stax/ui/theme/Color.kt index 7b00f4a9f..6eb724afc 100644 --- a/app/src/main/java/com/hover/stax/ui/theme/Color.kt +++ b/app/src/main/java/com/hover/stax/ui/theme/Color.kt @@ -2,11 +2,14 @@ package com.hover.stax.ui.theme import androidx.compose.ui.graphics.Color -val ColorPrimary = Color(0xFF292E35) -val ColorPrimaryDark = Color(0xFF1E232A) -val BrightBlue = Color(0xFF39CBFC) -val BrightBluePressed = Color(0xFF2AAFDC) +val ColorPrimary = Color(0xFF292E34) +val ColorPrimaryDark = Color(0xFF1E2329) +val BrightBlue = Color(0xFF0091E3) +val ColorSurface = Color(0x292E34) +val BrightBluePressed = Color(0xFF01659E) val OffWhite = Color(0xFFF1F1F4) -val CardViewColor = Color(0xFF292E35) -val StaxStateRed = Color(0xFFFF0028) +val CardViewColor = Color(0xFF292E34) +val StaxStateRed = Color(0xFFF32345) val DarkGray = Color(0xFF777777) +val mainBackground = Color(0xFF1E2329) + diff --git a/app/src/main/java/com/hover/stax/ui/theme/Theme.kt b/app/src/main/java/com/hover/stax/ui/theme/Theme.kt index f3ca24b80..c09f97667 100644 --- a/app/src/main/java/com/hover/stax/ui/theme/Theme.kt +++ b/app/src/main/java/com/hover/stax/ui/theme/Theme.kt @@ -14,7 +14,7 @@ private val DarkColorPalette = darkColors( onSecondary= CardViewColor, surface = CardViewColor, onSurface = OffWhite, - background = ColorPrimaryDark, + background = mainBackground, onBackground = OffWhite, error = StaxStateRed, onError = OffWhite @@ -28,7 +28,7 @@ private val LightColorPalette = lightColors( onSecondary= CardViewColor, surface = CardViewColor, onSurface = OffWhite, - background = ColorPrimaryDark, + background = mainBackground, onBackground = OffWhite, error = StaxStateRed, onError = OffWhite diff --git a/app/src/main/java/com/hover/stax/ui/theme/Type.kt b/app/src/main/java/com/hover/stax/ui/theme/Type.kt index 7e9c51ba0..a6e32dc0e 100644 --- a/app/src/main/java/com/hover/stax/ui/theme/Type.kt +++ b/app/src/main/java/com/hover/stax/ui/theme/Type.kt @@ -25,6 +25,11 @@ val Typography = Typography( fontWeight = FontWeight.Normal, fontSize = 15.sp ), + subtitle2 = TextStyle( + fontFamily = Brutalista, + fontWeight = FontWeight.Normal, + fontSize = 14.sp + ), h1 = TextStyle( fontFamily = Brutalista, fontWeight = FontWeight.Medium, @@ -40,9 +45,18 @@ val Typography = Typography( fontWeight = FontWeight.Medium, fontSize = 19.sp ), + h4 = TextStyle( + fontFamily = Brutalista, + fontWeight = FontWeight.Medium, + fontSize = 18.sp + ), button = TextStyle( fontFamily = Brutalista, fontWeight = FontWeight.Medium, fontSize = 17.sp + ), + caption = TextStyle( + fontFamily = Brutalista, + fontWeight = FontWeight.Normal ) ) \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/ussd_library/LibraryFragment.kt b/app/src/main/java/com/hover/stax/ussd_library/LibraryFragment.kt index 4a76da429..5e0a0daaa 100644 --- a/app/src/main/java/com/hover/stax/ussd_library/LibraryFragment.kt +++ b/app/src/main/java/com/hover/stax/ussd_library/LibraryFragment.kt @@ -8,15 +8,23 @@ import android.view.LayoutInflater import android.view.View import android.view.View.VISIBLE import android.view.ViewGroup +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.core.text.HtmlCompat import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController import com.hover.stax.R import com.hover.stax.addChannels.ChannelsViewModel import com.hover.stax.channels.Channel import com.hover.stax.countries.CountryAdapter import com.hover.stax.databinding.FragmentLibraryBinding +import com.hover.stax.presentation.home.TopBar +import com.hover.stax.transactions.TransactionHistoryFragmentDirections +import com.hover.stax.ui.theme.StaxTheme import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.UIHelper +import com.hover.stax.utils.network.NetworkMonitor import com.hover.stax.views.RequestServiceDialog import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber @@ -39,6 +47,8 @@ class LibraryFragment : Fragment(), CountryAdapter.SelectListener, LibraryChanne super.onViewCreated(view, savedInstanceState) AnalyticsUtil.logAnalyticsEvent(getString(R.string.visit_screen, LibraryFragment::class.java.simpleName), requireActivity()) + initToolbar() + binding.countryCard.showProgressIndicator() binding.countryDropdown.setListener(this) @@ -52,6 +62,20 @@ class LibraryFragment : Fragment(), CountryAdapter.SelectListener, LibraryChanne setObservers() } + private fun initToolbar() { + binding.toolbar.setContent { + StaxTheme { Toolbar() } + } + } + + @Composable + private fun Toolbar() { + val hasNetwork by NetworkMonitor.StateLiveData.get().observeAsState(initial = false) + TopBar(title = R.string.library_cardhead, isInternetConnected = hasNetwork) { + findNavController().navigate(TransactionHistoryFragmentDirections.actionGlobalNavigationSettings()) + } + } + private fun setObservers() { with(viewModel) { channelCountryList.observe(viewLifecycleOwner) { it?.let { binding.countryDropdown.updateChoices(it, countryChoice.value) } } @@ -94,7 +118,7 @@ class LibraryFragment : Fragment(), CountryAdapter.SelectListener, LibraryChanne } private fun showEmptyState() { - val content = resources.getString(R.string.no_accounts_found_desc, viewModel.filterQuery.value!!) + val content = resources.getString(R.string.no_accounts_found_desc, viewModel.filterQuery.value ?: getString(R.string.empty_channel_placeholder)) binding.emptyState.noAccountFoundDesc.apply { text = HtmlCompat.fromHtml(content, HtmlCompat.FROM_HTML_MODE_LEGACY) movementMethod = LinkMovementMethod.getInstance() diff --git a/app/src/main/java/com/hover/stax/utils/UIHelper.kt b/app/src/main/java/com/hover/stax/utils/UIHelper.kt index 7103f6226..d85bfc12c 100644 --- a/app/src/main/java/com/hover/stax/utils/UIHelper.kt +++ b/app/src/main/java/com/hover/stax/utils/UIHelper.kt @@ -24,10 +24,9 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.request.target.CustomTarget import com.google.android.material.snackbar.Snackbar import com.hover.stax.R -import com.hover.stax.accounts.Account +import com.hover.stax.domain.model.Account import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.launch import timber.log.Timber @@ -35,22 +34,35 @@ object UIHelper { private const val INITIAL_ITEMS_FETCH = 30 - fun flashMessage(context: Context, view: View?, message: String?) { - if (view == null) flashMessage(context, message) else showSnack(view, message) + fun showAndReportSnackBar(context: Context, view: View?, message: String) { + if (view == null) flashAndReportMessage(context, message) else showSnack(view, message) } private fun showSnack(view: View, message: String?) { val s = Snackbar.make(view, message!!, Snackbar.LENGTH_LONG) s.anchorView = view s.show() + AnalyticsUtil.logAnalyticsEvent(message, view.context) } - fun flashMessage(context: Context, message: String?) { + fun flashAndReportMessage(context: Context, messageRes: Int) { + flashAndReportMessage(context, context.getString(messageRes)) + } + + fun flashAndReportMessage(context: Context, message: String) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + AnalyticsUtil.logAnalyticsEvent(message, context) } - fun flashMessage(context: Context, messageRes: Int) { - Toast.makeText(context, context.getString(messageRes), Toast.LENGTH_SHORT).show() + fun flashAndReportError(context: Context, messageRes: Int) { + val message = context.getString(messageRes) + flashAndReportError(context, message) + } + + fun flashAndReportError(context: Context, message: String) { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + AnalyticsUtil.logAnalyticsEvent(message, context) + AnalyticsUtil.logErrorAndReportToFirebase(context.getString(R.string.toast_err_tag), message, null) } fun setMainLinearManagers(context: Context?): LinearLayoutManager { @@ -121,16 +133,11 @@ object UIHelper { } -fun Fragment.collectLatestLifecycleFlow(flow: Flow, collect: suspend (T) -> Unit) { +fun Fragment.collectLifecycleFlow(flow: Flow, collector: FlowCollector) { viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - flow.collect(collect) + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + flow.collect(collector) } } } -//fun Fragment.collectLatestSharedFlow(flow: SharedFlow, collect: suspend (T) -> Unit) { -// lifecycleScope.launchWhenStarted { -// flow.collect { collect } -// } -//} diff --git a/app/src/main/java/com/hover/stax/utils/Utils.kt b/app/src/main/java/com/hover/stax/utils/Utils.kt index 464e5c059..dc56ab219 100644 --- a/app/src/main/java/com/hover/stax/utils/Utils.kt +++ b/app/src/main/java/com/hover/stax/utils/Utils.kt @@ -137,7 +137,7 @@ object Utils { return getBuildConfigValue(c, "DEBUG") as Boolean } - private fun getBuildConfigValue(context: Context, fieldName: String?): Any? { + private fun getBuildConfigValue(context: Context, fieldName: String): Any? { try { val clazz = Class.forName(getPackage(context) + ".BuildConfig") val field = clazz.getField(fieldName) @@ -154,7 +154,7 @@ object Utils { val clip = ClipData.newPlainText("Stax content", content) if (clipboard != null) { clipboard.setPrimaryClip(clip) - UIHelper.flashMessage(c, c.getString(R.string.copied)) + UIHelper.flashAndReportMessage(c, c.getString(R.string.copied)) return true } return false @@ -199,7 +199,7 @@ object Utils { context.startActivity(intent) } catch (e: ActivityNotFoundException) { Timber.e("Activity not found") - UIHelper.flashMessage(context, context.getString(R.string.email_client_not_found)) + UIHelper.flashAndReportMessage(context, context.getString(R.string.email_client_not_found)) } } @@ -241,6 +241,6 @@ object Utils { if (PermissionUtils.has(arrayOf(Manifest.permission.CALL_PHONE), c)) c.startActivity(dialIntent) else - UIHelper.flashMessage(c, c.getString(R.string.enable_call_permission)) + UIHelper.flashAndReportMessage(c, c.getString(R.string.enable_call_permission)) } } \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/views/StaxCardView.kt b/app/src/main/java/com/hover/stax/views/StaxCardView.kt index 8175604d3..063b5df06 100644 --- a/app/src/main/java/com/hover/stax/views/StaxCardView.kt +++ b/app/src/main/java/com/hover/stax/views/StaxCardView.kt @@ -34,16 +34,15 @@ open class StaxCardView(context: Context, attrs: AttributeSet) : FrameLayout(con useContextBackPress = a.getBoolean(R.styleable.StaxCardView_defaultBackPress, true) backDrawable = a.getResourceId(R.styleable.StaxCardView_backRes, 0) bgColor = a.getColor(R.styleable.StaxCardView_staxCardColor, ContextCompat.getColor(context, R.color.colorPrimary)) - isFlatView = a.getBoolean(R.styleable.StaxCardView_isFlatView, false) + isFlatView = a.getBoolean(R.styleable.StaxCardView_isFlatView, true) } finally { a.recycle() } } - fun makeFlatView() { + private fun makeFlatView() { val zero = 0 - binding.cardViewHeader.cardElevation = zero.toFloat() - binding.cardViewHeader.radius = zero.toFloat() + binding.cardView.cardElevation = zero.toFloat() removeCardMargin() } @@ -115,7 +114,7 @@ open class StaxCardView(context: Context, attrs: AttributeSet) : FrameLayout(con private fun removeCardMargin() { val params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) params.setMargins(0, 0, 0, 0) - binding.cardViewHeader.layoutParams = params + binding.cardView.layoutParams = params } override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams) { @@ -135,7 +134,7 @@ open class StaxCardView(context: Context, attrs: AttributeSet) : FrameLayout(con } fun updateState(icon: Int, backgroundColor: Int, title: Int) { - binding.cardViewHeader.apply { + binding.cardView.apply { setBackButtonVisibility(View.VISIBLE) setIcon(icon) setTitle(title) diff --git a/app/src/main/java/com/hover/stax/views/StaxDropdownLayout.kt b/app/src/main/java/com/hover/stax/views/StaxDropdownLayout.kt index b6ede5ad7..c529efd39 100644 --- a/app/src/main/java/com/hover/stax/views/StaxDropdownLayout.kt +++ b/app/src/main/java/com/hover/stax/views/StaxDropdownLayout.kt @@ -46,7 +46,7 @@ open class StaxDropdownLayout(context: Context, attrs: AttributeSet): AbstractSt if (helperText != null) binding.inputLayout.hint = helperText.toString() } - override fun initView() { + final override fun initView() { super.initView() autoCompleteTextView = binding.autoCompleteView } diff --git a/app/src/main/java/com/hover/stax/views/StaxTextInput.kt b/app/src/main/java/com/hover/stax/views/StaxTextInput.kt index 99ac94b70..c1ce779f3 100644 --- a/app/src/main/java/com/hover/stax/views/StaxTextInput.kt +++ b/app/src/main/java/com/hover/stax/views/StaxTextInput.kt @@ -54,7 +54,7 @@ class StaxTextInput(context: Context, attrs: AttributeSet) : AbstractStatefulInp if (inputType > 0) binding?.inputEditText?.inputType = inputType } - fun setMutlipartText(text: String?, subtext: String?) { + fun setMultipartText(text: String?, subtext: String?) { if (text.isNullOrEmpty()) setText(subtext) else if (subtext.isNullOrEmpty()) @@ -88,18 +88,10 @@ class StaxTextInput(context: Context, attrs: AttributeSet) : AbstractStatefulInp } fun addTextChangedListener(listener: TextWatcher) { -// textWatcher = object : TextWatcher { -// override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) { listener.beforeTextChanged(charSequence, i, i1, i2)} -// override fun afterTextChanged(editable: Editable) { listener.afterTextChanged(editable)} -// override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) { -// currentText = charSequence.toString() -// Timber.e("watcher for $hint got an update: %s", currentText) -// listener.onTextChanged(charSequence, i, i1, i2) -// } -// } editText?.addTextChangedListener(listener) } + @SuppressLint("ClickableViewAccessibility") override fun setOnClickListener(listener: OnClickListener?) { editText?.setOnTouchListener { _, event -> diff --git a/app/src/main/java/com/hover/stax/views/staxcardstack/StaxCardStackAdapter.java b/app/src/main/java/com/hover/stax/views/staxcardstack/StaxCardStackAdapter.java deleted file mode 100755 index 4c9f9678e..000000000 --- a/app/src/main/java/com/hover/stax/views/staxcardstack/StaxCardStackAdapter.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.hover.stax.views.staxcardstack; - -import android.content.Context; -import android.view.LayoutInflater; - -import java.util.ArrayList; -import java.util.List; - -public abstract class StaxCardStackAdapter extends StaxCardStackView.Adapter { - - private final Context mContext; - private final LayoutInflater mInflater; - private final List mData; - - public StaxCardStackAdapter(Context context) { - this.mContext = context; - this.mInflater = LayoutInflater.from(context); - this.mData = new ArrayList<>(); - } - - public void updateData(List data) { - this.setData(data); - this.notifyDataSetChanged(); - } - - public void setData(List data) { - this.mData.clear(); - if (data != null) { - this.mData.addAll(data); - } - } - - public LayoutInflater getLayoutInflater() { - return this.mInflater; - } - - public Context getContext() { - return this.mContext; - } - - @Override - public void onBindViewHolder(StaxCardStackView.ViewHolder holder, int position) { - T data = this.getItem(position); - this.bindView(data, position, holder); - } - - public abstract void bindView(T data, int position, StaxCardStackView.ViewHolder holder); - - @Override - public int getItemCount() { - return mData.size(); - } - - public T getItem(int position) { - return this.mData.get(position); - } - -} diff --git a/app/src/main/java/com/hover/stax/views/staxcardstack/StaxCardStackView.java b/app/src/main/java/com/hover/stax/views/staxcardstack/StaxCardStackView.java deleted file mode 100755 index 8d03b7e82..000000000 --- a/app/src/main/java/com/hover/stax/views/staxcardstack/StaxCardStackView.java +++ /dev/null @@ -1,266 +0,0 @@ -package com.hover.stax.views.staxcardstack; - -import android.content.Context; -import android.content.res.TypedArray; -import android.database.Observable; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; - -import com.hover.stax.R; - -import java.util.ArrayList; -import java.util.List; - -public class StaxCardStackView extends ViewGroup { - public static final int INVALID_TYPE = -1; - static final int DEFAULT_SELECT_POSITION = -1; - private static final String TAG = "CardStackView"; - - private final ViewDataObserver mObserver = new ViewDataObserver(); - private int mTotalLength; - private int mOverlapGaps; - private StaxCardStackAdapter mStaxCardStackAdapter; - private int mShowHeight; - private List mViewHolders; - - public StaxCardStackView(Context context) { - this(context, null); - } - - public StaxCardStackView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public StaxCardStackView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context, attrs, defStyleAttr, 0); - } - - private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CardStackView, defStyleAttr, defStyleRes); - setOverlapGaps(array.getDimensionPixelSize(R.styleable.CardStackView_stackOverlapGaps, dp2px(20))); - array.recycle(); - - mViewHolders = new ArrayList<>(); - } - - private int dp2px(int value) { - final float scale = getContext().getResources().getDisplayMetrics().density; - return (int) (value * scale + 0.5f); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - checkContentHeightByParent(); - measureChild(widthMeasureSpec, heightMeasureSpec); - } - - private void checkContentHeightByParent() { - View parentView = (View) getParent(); - mShowHeight = parentView.getMeasuredHeight() - parentView.getPaddingTop() - parentView.getPaddingBottom(); - } - - private void measureChild(int widthMeasureSpec, int heightMeasureSpec) { - int maxWidth = 0; - mTotalLength = 0; - mTotalLength += getPaddingTop() + getPaddingBottom(); - for (int i = 0; i < getChildCount(); i++) { - final View child = getChildAt(i); - measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); - final int totalLength = mTotalLength; - final LayoutParams lp = - (LayoutParams) child.getLayoutParams(); - if (lp.mHeaderHeight == -1) lp.mHeaderHeight = child.getMeasuredHeight(); - final int childHeight = lp.mHeaderHeight; - mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + - lp.bottomMargin); - mTotalLength -= mOverlapGaps * 2; - final int margin = lp.leftMargin + lp.rightMargin; - final int measuredWidth = child.getMeasuredWidth() + margin; - maxWidth = Math.max(maxWidth, measuredWidth); - } - - mTotalLength += mOverlapGaps * 2; - int heightSize = mTotalLength; - heightSize = Math.max(heightSize, mShowHeight); - int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); - setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0), - heightSizeAndState); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - layoutChild(); - } - - private void layoutChild() { - int childTop = getPaddingTop(); - int childLeft = getPaddingLeft(); - - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - final int childWidth = child.getMeasuredWidth(); - int childHeight = child.getMeasuredHeight(); - - final LayoutParams lp = - (LayoutParams) child.getLayoutParams(); - childTop += lp.topMargin; - if (i != 0) { - childTop -= mOverlapGaps * 2; - child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); - } else { - child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); - } - childTop += lp.mHeaderHeight; - } - } - - - public void setAdapter(StaxCardStackAdapter staxCardStackAdapter) { - mStaxCardStackAdapter = staxCardStackAdapter; - mStaxCardStackAdapter.registerObserver(mObserver); - refreshView(); - } - - private void refreshView() { - removeAllViews(); - mViewHolders.clear(); - for (int i = 0; i < mStaxCardStackAdapter.getItemCount(); i++) { - ViewHolder holder = getViewHolder(i); - holder.position = i; - addView(holder.itemView); - mStaxCardStackAdapter.bindViewHolder(holder, i); - } - requestLayout(); - } - - ViewHolder getViewHolder(int i) { - if (i == DEFAULT_SELECT_POSITION) return null; - ViewHolder viewHolder; - if (mViewHolders.size() <= i || mViewHolders.get(i).mItemViewType != mStaxCardStackAdapter.getItemViewType(i)) { - viewHolder = mStaxCardStackAdapter.createView(this, mStaxCardStackAdapter.getItemViewType(i)); - mViewHolders.add(viewHolder); - } else { - viewHolder = mViewHolders.get(i); - } - return viewHolder; - } - - @Override - public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { - return new LayoutParams(getContext(), attrs); - } - - @Override - protected ViewGroup.LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - } - - @Override - protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { - return new LayoutParams(p); - } - - @Override - protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { - return p instanceof LayoutParams; - } - - public void setOverlapGaps(int overlapGaps) { - mOverlapGaps = overlapGaps; - } - - public static class LayoutParams extends MarginLayoutParams { - - public int mHeaderHeight; - - public LayoutParams(Context c, AttributeSet attrs) { - super(c, attrs); - - TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.CardStackView); - mHeaderHeight = array.getDimensionPixelSize(R.styleable.CardStackView_stackHeaderHeight, -1); - } - - public LayoutParams(int width, int height) { - super(width, height); - } - - public LayoutParams(ViewGroup.LayoutParams source) { - super(source); - } - } - - public static abstract class Adapter { - private final AdapterDataObservable mObservable = new AdapterDataObservable(); - - VH createView(ViewGroup parent, int viewType) { - VH holder = onCreateView(parent, viewType); - holder.mItemViewType = viewType; - return holder; - } - - protected abstract VH onCreateView(ViewGroup parent, int viewType); - - public void bindViewHolder(VH holder, int position) { - onBindViewHolder(holder, position); - } - - protected abstract void onBindViewHolder(VH holder, int position); - - public abstract int getItemCount(); - - public int getItemViewType(int position) { - return 0; - } - - public final void notifyDataSetChanged() { - mObservable.notifyChanged(); - } - - public void registerObserver(AdapterDataObserver observer) { - mObservable.registerObserver(observer); - } - } - - public static abstract class ViewHolder { - - public View itemView; - int mItemViewType = INVALID_TYPE; - int position; - - public ViewHolder(View view) { - itemView = view; - } - - public Context getContext() { - return itemView.getContext(); - } - } - - public static class AdapterDataObservable extends Observable { - public boolean hasObservers() { - return !mObservers.isEmpty(); - } - - public void notifyChanged() { - for (int i = mObservers.size() - 1; i >= 0; i--) { - mObservers.get(i).onChanged(); - } - } - } - - public static abstract class AdapterDataObserver { - public void onChanged() { - } - } - - private class ViewDataObserver extends AdapterDataObserver { - @Override - public void onChanged() { - refreshView(); - } - } - -} diff --git a/app/src/main/res/drawable/airtime_illustration.png b/app/src/main/res/drawable/airtime_illustration.png deleted file mode 100644 index 5839d788b..000000000 Binary files a/app/src/main/res/drawable/airtime_illustration.png and /dev/null differ diff --git a/app/src/main/res/drawable/button_bg_grey.xml b/app/src/main/res/drawable/button_bg_grey.xml index 46deb499b..64cc2d951 100644 --- a/app/src/main/res/drawable/button_bg_grey.xml +++ b/app/src/main/res/drawable/button_bg_grey.xml @@ -9,7 +9,7 @@ - + diff --git a/app/src/main/res/drawable/ic_add_white_16.xml b/app/src/main/res/drawable/ic_add_white_16.xml new file mode 100644 index 000000000..70046c48f --- /dev/null +++ b/app/src/main/res/drawable/ic_add_white_16.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_send_money.xml b/app/src/main/res/drawable/ic_send_money.xml new file mode 100644 index 000000000..238cf89e8 --- /dev/null +++ b/app/src/main/res/drawable/ic_send_money.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_shopping_cart.xml b/app/src/main/res/drawable/ic_shopping_cart.xml new file mode 100644 index 000000000..aeda9020e --- /dev/null +++ b/app/src/main/res/drawable/ic_shopping_cart.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_tip_of_day.xml b/app/src/main/res/drawable/ic_tip_of_day.xml new file mode 100644 index 000000000..adc746293 --- /dev/null +++ b/app/src/main/res/drawable/ic_tip_of_day.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/img_placeholder.xml b/app/src/main/res/drawable/img_placeholder.xml new file mode 100644 index 000000000..6aeeab6ac --- /dev/null +++ b/app/src/main/res/drawable/img_placeholder.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/offline_illustration.png b/app/src/main/res/drawable/offline_illustration.png deleted file mode 100644 index cc9976c38..000000000 Binary files a/app/src/main/res/drawable/offline_illustration.png and /dev/null differ diff --git a/app/src/main/res/drawable/request_illustration.png b/app/src/main/res/drawable/request_illustration.png deleted file mode 100644 index 0253efc69..000000000 Binary files a/app/src/main/res/drawable/request_illustration.png and /dev/null differ diff --git a/app/src/main/res/drawable/send_illustration.png b/app/src/main/res/drawable/send_illustration.png deleted file mode 100644 index 197544007..000000000 Binary files a/app/src/main/res/drawable/send_illustration.png and /dev/null differ diff --git a/app/src/main/res/drawable/splash_logo.xml b/app/src/main/res/drawable/splash_logo.xml deleted file mode 100644 index 3518aab66..000000000 --- a/app/src/main/res/drawable/splash_logo.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/stax_logo.xml b/app/src/main/res/drawable/stax_logo.xml new file mode 100644 index 000000000..2ccb848fe --- /dev/null +++ b/app/src/main/res/drawable/stax_logo.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/main/res/drawable/stax_slide_2.jpg b/app/src/main/res/drawable/stax_slide_2.jpg deleted file mode 100644 index 075f374d9..000000000 Binary files a/app/src/main/res/drawable/stax_slide_2.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/tips_fancy_icon.png b/app/src/main/res/drawable/tips_fancy_icon.png new file mode 100644 index 000000000..b6c4dc5c0 Binary files /dev/null and b/app/src/main/res/drawable/tips_fancy_icon.png differ diff --git a/app/src/main/res/layout/account_card_manage.xml b/app/src/main/res/layout/account_card_manage.xml index 16bf933ed..d872e0baf 100644 --- a/app/src/main/res/layout/account_card_manage.xml +++ b/app/src/main/res/layout/account_card_manage.xml @@ -3,6 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/margin_16" app:title="@string/manage_account"> - diff --git a/app/src/main/res/layout/balance_fragment_container.xml b/app/src/main/res/layout/balance_fragment_container.xml new file mode 100644 index 000000000..0f12ed4a8 --- /dev/null +++ b/app/src/main/res/layout/balance_fragment_container.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/balance_item.xml b/app/src/main/res/layout/balance_item.xml index 862d61d87..141035a6b 100644 --- a/app/src/main/res/layout/balance_item.xml +++ b/app/src/main/res/layout/balance_item.xml @@ -6,11 +6,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/margin_13" - android:paddingStart="@dimen/margin_13" - android:paddingEnd="0dp" - app:cardBackgroundColor="@color/cardViewColor" - app:cardCornerRadius="@dimen/radius" - app:cardElevation="8dp" + app:cardBackgroundColor="@color/colorBackground" app:cardPreventCornerOverlap="false"> + app:cardCornerRadius="@dimen/radius"> + app:cardElevation="0dp"> @@ -30,11 +30,11 @@ android:id="@+id/title" android:layout_width="match_parent" android:layout_height="wrap_content" - app:fontFamily="@font/brutalista_medium" - android:gravity="start" android:layout_marginBottom="@dimen/margin_8" + android:gravity="start" android:textColor="@color/banner_text" android:textSize="20sp" + app:fontFamily="@font/brutalista_medium" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="This is a test title" /> @@ -51,29 +51,20 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/title" tools:text="This is test content" /> - - - - + app:fontFamily="@font/brutalista_medium" /> + + diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml index 565fa4217..702701c9f 100644 --- a/app/src/main/res/layout/fragment_account.xml +++ b/app/src/main/res/layout/fragment_account.xml @@ -7,6 +7,7 @@ @@ -65,12 +66,12 @@ + + diff --git a/app/src/main/res/layout/fragment_add_channels.xml b/app/src/main/res/layout/fragment_add_channels.xml index 0b95b5113..ad1d786a0 100644 --- a/app/src/main/res/layout/fragment_add_channels.xml +++ b/app/src/main/res/layout/fragment_add_channels.xml @@ -8,32 +8,12 @@ android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="0dp" + android:paddingHorizontal="@dimen/margin_5" android:layout_marginBottom="@dimen/margin_13" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - - - - - - - - -