diff --git a/.idea/misc.xml b/.idea/misc.xml index 87aba43ea..e545927ed 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,15 +10,24 @@ + + + + + + + + + @@ -30,7 +39,18 @@ + + + + + + + + + + + @@ -49,6 +69,7 @@ + @@ -85,7 +106,7 @@ - + @@ -94,6 +115,7 @@ + @@ -152,12 +174,12 @@ - + - + @@ -188,7 +210,7 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 8eeb569eb..6a7d38db6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,8 +45,8 @@ android { applicationId "com.hover.stax" minSdk 21 targetSdk 31 - versionCode 137 - versionName "1.10.4" + versionCode 138 + versionName "1.11.0" vectorDrawables.useSupportLibrary = true multiDexEnabled true diff --git a/app/schemas/com.hover.stax.database.AppDatabase/39.json b/app/schemas/com.hover.stax.database.AppDatabase/39.json new file mode 100644 index 000000000..e91fb7369 --- /dev/null +++ b/app/schemas/com.hover.stax.database.AppDatabase/39.json @@ -0,0 +1,846 @@ +{ + "formatVersion": 1, + "database": { + "version": 39, + "identityHash": "20d754ea921bd26d6f1996cb90a140d9", + "entities": [ + { + "tableName": "channels", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `country_alpha2` TEXT NOT NULL, `root_code` TEXT, `currency` TEXT NOT NULL, `hni_list` TEXT NOT NULL, `logo_url` TEXT NOT NULL, `institution_id` INTEGER NOT NULL, `primary_color_hex` TEXT NOT NULL, `published` INTEGER NOT NULL DEFAULT 0, `secondary_color_hex` TEXT NOT NULL, `selected` INTEGER NOT NULL DEFAULT 0, `defaultAccount` INTEGER NOT NULL DEFAULT 0, `pin` TEXT, `latestBalance` TEXT, `latestBalanceTimestamp` INTEGER DEFAULT CURRENT_TIMESTAMP, `account_no` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "countryAlpha2", + "columnName": "country_alpha2", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rootCode", + "columnName": "root_code", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hniList", + "columnName": "hni_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logo_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "institutionId", + "columnName": "institution_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryColorHex", + "columnName": "primary_color_hex", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "secondaryColorHex", + "columnName": "secondary_color_hex", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "selected", + "columnName": "selected", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "defaultAccount", + "columnName": "defaultAccount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pin", + "columnName": "pin", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestBalance", + "columnName": "latestBalance", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestBalanceTimestamp", + "columnName": "latestBalanceTimestamp", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "CURRENT_TIMESTAMP" + }, + { + "fieldPath": "accountNo", + "columnName": "account_no", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "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', `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, `account_id` INTEGER, `counterparty` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "action_id", + "columnName": "action_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "environment", + "columnName": "environment", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "transaction_type", + "columnName": "transaction_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "channel_id", + "columnName": "channel_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'pending'" + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "initiated_at", + "columnName": "initiated_at", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "CURRENT_TIMESTAMP" + }, + { + "fieldPath": "updated_at", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "CURRENT_TIMESTAMP" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "fee", + "columnName": "fee", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "confirm_code", + "columnName": "confirm_code", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "counterparty_id", + "columnName": "recipient_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "balance", + "columnName": "balance", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountId", + "columnName": "account_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "counterparty", + "columnName": "counterparty", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_stax_transactions_uuid", + "unique": true, + "columnNames": [ + "uuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_stax_transactions_uuid` ON `${TABLE_NAME}` (`uuid`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "stax_contacts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lookup_key` TEXT, `name` TEXT, `aliases` TEXT, `phone_number` TEXT, `thumb_uri` TEXT, `last_used_timestamp` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lookupKey", + "columnName": "lookup_key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "aliases", + "columnName": "aliases", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbUri", + "columnName": "thumb_uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUsedTimestamp", + "columnName": "last_used_timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_stax_contacts_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_stax_contacts_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_stax_contacts_phone_number", + "unique": true, + "columnNames": [ + "phone_number" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_stax_contacts_phone_number` ON `${TABLE_NAME}` (`phone_number`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "requests", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `description` TEXT, `requestee_ids` TEXT NOT NULL, `amount` TEXT, `requester_institution_id` INTEGER NOT NULL, `requester_number` TEXT, `note` TEXT, `message` TEXT, `matched_transaction_uuid` TEXT, `date_sent` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "requestee_ids", + "columnName": "requestee_ids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "requester_institution_id", + "columnName": "requester_institution_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requester_number", + "columnName": "requester_number", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "matched_transaction_uuid", + "columnName": "matched_transaction_uuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date_sent", + "columnName": "date_sent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "CURRENT_TIMESTAMP" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "schedules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `channel_id` INTEGER NOT NULL, `action_id` TEXT, `recipient_ids` TEXT NOT NULL, `amount` TEXT, `note` TEXT, `description` TEXT NOT NULL, `start_date` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, `end_date` INTEGER DEFAULT CURRENT_TIMESTAMP, `frequency` INTEGER NOT NULL, `complete` INTEGER NOT NULL DEFAULT false)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "channel_id", + "columnName": "channel_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "action_id", + "columnName": "action_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipient_ids", + "columnName": "recipient_ids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "start_date", + "columnName": "start_date", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "CURRENT_TIMESTAMP" + }, + { + "fieldPath": "end_date", + "columnName": "end_date", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "CURRENT_TIMESTAMP" + }, + { + "fieldPath": "frequency", + "columnName": "frequency", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "complete", + "columnName": "complete", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "accounts", + "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 DEFAULT 0, `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 NO ACTION )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alias", + "columnName": "alias", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logo_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountNo", + "columnName": "account_no", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryColorHex", + "columnName": "primary_color_hex", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondaryColorHex", + "columnName": "secondary_color_hex", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDefault", + "columnName": "isDefault", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestBalance", + "columnName": "latestBalance", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestBalanceTimestamp", + "columnName": "latestBalanceTimestamp", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "CURRENT_TIMESTAMP" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_accounts_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_accounts_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_accounts_channelId", + "unique": false, + "columnNames": [ + "channelId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_accounts_channelId` ON `${TABLE_NAME}` (`channelId`)" + } + ], + "foreignKeys": [ + { + "table": "channels", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "channelId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "paybills", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `business_no` TEXT NOT NULL, `account_no` TEXT, `channelId` INTEGER NOT NULL, `accountId` INTEGER NOT NULL, `logo_url` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recurring_amount` INTEGER NOT NULL, `logo` INTEGER NOT NULL, `isSaved` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`channelId`) REFERENCES `channels`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`accountId`) REFERENCES `accounts`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "businessNo", + "columnName": "business_no", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountNo", + "columnName": "account_no", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logo_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recurringAmount", + "columnName": "recurring_amount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logo", + "columnName": "logo", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSaved", + "columnName": "isSaved", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_paybills_business_no_account_no", + "unique": true, + "columnNames": [ + "business_no", + "account_no" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_paybills_business_no_account_no` ON `${TABLE_NAME}` (`business_no`, `account_no`)" + }, + { + "name": "index_paybills_channelId", + "unique": false, + "columnNames": [ + "channelId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_paybills_channelId` ON `${TABLE_NAME}` (`channelId`)" + }, + { + "name": "index_paybills_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_paybills_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "channels", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "channelId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "accounts", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "stax_users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `username` TEXT NOT NULL, `email` TEXT NOT NULL, `isMapper` INTEGER NOT NULL DEFAULT 0, `marketingOptedIn` INTEGER NOT NULL DEFAULT 0, `transactionCount` INTEGER NOT NULL, `bountyTotal` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isMapper", + "columnName": "isMapper", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "marketingOptedIn", + "columnName": "marketingOptedIn", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "transactionCount", + "columnName": "transactionCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bountyTotal", + "columnName": "bountyTotal", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "bonuses", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_channel` INTEGER NOT NULL, `purchase_channel` INTEGER NOT NULL, `bonus_percent` REAL NOT NULL, `message` TEXT NOT NULL, PRIMARY KEY(`user_channel`, `purchase_channel`))", + "fields": [ + { + "fieldPath": "userChannel", + "columnName": "user_channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "purchaseChannel", + "columnName": "purchase_channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bonusPercent", + "columnName": "bonus_percent", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_channel", + "purchase_channel" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '20d754ea921bd26d6f1996cb90a140d9')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e143455b7..0093424b9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + 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 a8d8bf619..1095b63c2 100644 --- a/app/src/main/java/com/hover/stax/accounts/AccountDropdown.kt +++ b/app/src/main/java/com/hover/stax/accounts/AccountDropdown.kt @@ -49,7 +49,7 @@ class AccountDropdown(context: Context, attributeSet: AttributeSet) : StaxDropdo } private fun accountUpdate(accounts: List) { - if (!accounts.isNullOrEmpty()) { + if (accounts.isNotEmpty()) { updateChoices(accounts) } else if (!hasExistingContent()) { setState(context.getString(R.string.accounts_error_no_accounts), NONE) @@ -90,11 +90,11 @@ class AccountDropdown(context: Context, attributeSet: AttributeSet) : StaxDropdo } } - fun setCurrentAccount() = onSelect(accountList.firstOrNull { it.isDefault }) + fun setCurrentAccount(account: Account? = null, channelId: Int = 0) = onSelect(account ?: accountList.firstOrNull { it.isDefault }, channelId) - private fun onSelect(account: Account?) { + private fun onSelect(account: Account?, channelOverride: Int = 0) { setDropdownValue(account) - account?.let { highlightListener?.highlightAccount(it) } + account?.let { highlightListener?.highlightAccount(it, channelOverride) } } private fun hasExistingContent(): Boolean = autoCompleteTextView.adapter != null && autoCompleteTextView.adapter.count > 0 @@ -118,6 +118,7 @@ class AccountDropdown(context: Context, attributeSet: AttributeSet) : StaxDropdo } val accountObserver = object: Observer { override fun onChanged(t: Account?) { + Timber.e("Active account right now is ${t?.name}") setDropdownValue(t) } } @@ -135,14 +136,14 @@ class AccountDropdown(context: Context, attributeSet: AttributeSet) : StaxDropdo private fun setState(actions: List, viewModel: ChannelsViewModel) { when { - viewModel.activeChannel.value != null && (actions.isNullOrEmpty()) -> setState( + viewModel.activeChannel.value != null && (actions.isEmpty()) -> setState( context.getString( R.string.no_actions_fielderror, HoverAction.getHumanFriendlyType(context, viewModel.getActionType()) ), ERROR ) - !actions.isNullOrEmpty() && actions.size == 1 && !actions.first().requiresRecipient() && viewModel.getActionType() != HoverAction.BALANCE -> + actions.isNotEmpty() && actions.size == 1 && !actions.first().requiresRecipient() && viewModel.getActionType() != HoverAction.BALANCE -> setState( context.getString( if (actions.first().transaction_type == HoverAction.AIRTIME) R.string.self_only_airtime_warning @@ -159,7 +160,7 @@ class AccountDropdown(context: Context, attributeSet: AttributeSet) : StaxDropdo } interface HighlightListener { - fun highlightAccount(account: Account) + fun highlightAccount(account: Account, channelOverride: Int = 0) } interface AccountFetchListener { 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 999f5e1c0..a27de5639 100644 --- a/app/src/main/java/com/hover/stax/accounts/AccountDropdownAdapter.kt +++ b/app/src/main/java/com/hover/stax/accounts/AccountDropdownAdapter.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import com.hover.stax.databinding.StaxSpinnerItemWithLogoBinding import com.hover.stax.utils.UIHelper +import com.hover.stax.utils.UIHelper.loadImage class AccountDropdownAdapter(val accounts: List, context: Context) : ArrayAdapter(context, 0, accounts) { @@ -38,8 +39,7 @@ class AccountDropdownAdapter(val accounts: List, context: Context) : Ar fun setAccount(account: Account) { binding.serviceItemNameId.text = account.alias - - UIHelper.loadImage(binding.root.context, account.logoUrl, binding.serviceItemImageId) + binding.serviceItemImageId.loadImage(binding.root.context, account.logoUrl) } } diff --git a/app/src/main/java/com/hover/stax/accounts/AccountsFragment.kt b/app/src/main/java/com/hover/stax/accounts/AccountsFragment.kt index 13e092a3f..84e885585 100644 --- a/app/src/main/java/com/hover/stax/accounts/AccountsFragment.kt +++ b/app/src/main/java/com/hover/stax/accounts/AccountsFragment.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.hover.sdk.actions.HoverAction import com.hover.stax.R +import com.hover.stax.bonus.BonusViewModel import com.hover.stax.channels.Channel import com.hover.stax.channels.ChannelsAdapter import com.hover.stax.channels.ChannelsViewModel @@ -17,6 +18,7 @@ import com.hover.stax.home.MainActivity import com.hover.stax.transfers.TransactionType import com.hover.stax.utils.UIHelper import kotlinx.coroutines.launch +import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.viewModel class AccountsFragment : Fragment(), ChannelsAdapter.SelectListener, AccountsAdapter.SelectListener { @@ -24,7 +26,10 @@ class AccountsFragment : Fragment(), ChannelsAdapter.SelectListener, AccountsAda private var _binding: FragmentAccountsBinding? = null private val binding get() = _binding!! - private val viewModel: ChannelsViewModel by viewModel() + private val selectAdapter = ChannelsAdapter(this) + + private val viewModel: ChannelsViewModel by sharedViewModel() + private val bonusViewModel: BonusViewModel by sharedViewModel() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentAccountsBinding.inflate(inflater, container, false) @@ -34,8 +39,6 @@ class AccountsFragment : Fragment(), ChannelsAdapter.SelectListener, AccountsAda override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val selectAdapter = ChannelsAdapter(this) - binding.accountsRV.apply { layoutManager = UIHelper.setMainLinearManagers(requireActivity()) adapter = selectAdapter @@ -44,8 +47,8 @@ class AccountsFragment : Fragment(), ChannelsAdapter.SelectListener, AccountsAda binding.accountListCard.setOnClickIcon { findNavController().popBackStack() } with(viewModel) { - allChannels.observe(viewLifecycleOwner) { selectAdapter.submitList(it) } - simChannels.observe(viewLifecycleOwner) { selectAdapter.submitList(it) } + allChannels.observe(viewLifecycleOwner) { filterList(it) } + simChannels.observe(viewLifecycleOwner) { filterList(it) } accounts.observe(viewLifecycleOwner) { if (it.isNotEmpty()) showAccountsList(it) @@ -53,6 +56,17 @@ class AccountsFragment : Fragment(), ChannelsAdapter.SelectListener, AccountsAda } } + private fun filterList(channels:List) { + val bonusChannelIds = bonusViewModel.bonuses.value?.map { it.purchaseChannel } + + val list = if(!bonusChannelIds.isNullOrEmpty()) + channels.filterNot { bonusChannelIds.contains(it.id) } + else + channels + + selectAdapter.submitList(list) + } + override fun clickedChannel(channel: Channel) { viewModel.setChannelsSelected(listOf(channel)) diff --git a/app/src/main/java/com/hover/stax/actions/ActionDropdownAdapter.kt b/app/src/main/java/com/hover/stax/actions/ActionDropdownAdapter.kt index 883534b7d..4fcb93bf6 100644 --- a/app/src/main/java/com/hover/stax/actions/ActionDropdownAdapter.kt +++ b/app/src/main/java/com/hover/stax/actions/ActionDropdownAdapter.kt @@ -13,6 +13,7 @@ import com.hover.stax.R import com.hover.stax.accounts.Account import com.hover.stax.databinding.StaxSpinnerItemWithLogoBinding import com.hover.stax.utils.UIHelper +import com.hover.stax.utils.UIHelper.loadImage class ActionDropdownAdapter(val actions: List, context: Context) : ArrayAdapter(context, 0, actions) { @@ -65,7 +66,7 @@ class ActionDropdownAdapter(val actions: List, context: Context) : ArrayAd logoUrl = baseUrl.plus(action.logoUrl) } - UIHelper.loadImage(binding.root.context, logoUrl!!, logo!!) + logo!!.loadImage(binding.root.context, logoUrl!!) } } diff --git a/app/src/main/java/com/hover/stax/actions/ActionSelect.kt b/app/src/main/java/com/hover/stax/actions/ActionSelect.kt index 8e4c9cdc9..a6ca42f93 100644 --- a/app/src/main/java/com/hover/stax/actions/ActionSelect.kt +++ b/app/src/main/java/com/hover/stax/actions/ActionSelect.kt @@ -28,7 +28,6 @@ class ActionSelect(context: Context, attrs: AttributeSet) : LinearLayout(context private val binding get() = _binding!! init { - Timber.e("Initi action select. highlighted?: %s", highlightedAction?.id.toString()) _binding = ActionSelectBinding.inflate(LayoutInflater.from(context), this, true) createListeners() visibility = GONE @@ -41,8 +40,8 @@ class ActionSelect(context: Context, attrs: AttributeSet) : LinearLayout(context } fun updateActions(filteredActions: List) { - visibility = if (filteredActions.isNullOrEmpty()) GONE else VISIBLE - if (filteredActions.isNullOrEmpty()) return + visibility = if (filteredActions.isEmpty()) GONE else VISIBLE + if (filteredActions.isEmpty()) return allActions = filteredActions if (!filteredActions.contains(highlightedAction)) @@ -58,7 +57,9 @@ class ActionSelect(context: Context, attrs: AttributeSet) : LinearLayout(context fun sort(actions: List): List = actions.distinctBy { it.to_institution_id }.toList() - private fun showRecipientNetwork(actions: List) = actions.size > 1 || (actions.size == 1 && !actions.first().isOnNetwork) + private fun showRecipientNetwork(actions: List): Boolean { + return actions.size > 1 || (actions.size == 1 && !actions.first().isOnNetwork) + } fun selectRecipientNetwork(action: HoverAction) { if (action == highlightedAction) return @@ -85,15 +86,7 @@ class ActionSelect(context: Context, attrs: AttributeSet) : LinearLayout(context } private fun getWhoMeOptions(recipientInstId: Int): List { - val options = ArrayList() - if (allActions == null) return options - - for (action in allActions!!) { - if (action.to_institution_id == recipientInstId && !options.contains(action)) - options.add(action) - } - - return options + return allActions?.filter { it.to_institution_id == recipientInstId } ?: emptyList() } private fun selectOnlyOption(option: HoverAction) { 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 8c5570ebb..adbc87eed 100644 --- a/app/src/main/java/com/hover/stax/actions/ActionSelectViewModel.kt +++ b/app/src/main/java/com/hover/stax/actions/ActionSelectViewModel.kt @@ -7,6 +7,7 @@ import com.hover.sdk.actions.HoverAction import com.hover.sdk.actions.HoverAction.* import com.hover.stax.R import com.hover.stax.utils.Constants +import java.util.LinkedHashMap class ActionSelectViewModel(private val application: Application) : ViewModel() { @@ -20,7 +21,7 @@ class ActionSelectViewModel(private val application: Application) : ViewModel() } private fun setActiveActionIfOutOfDate(actions: List) { - if (!actions.isNullOrEmpty() && (activeAction.value == null || !actions.contains(activeAction.value!!))) { + if (actions.isNotEmpty() && (activeAction.value == null || !actions.contains(activeAction.value!!))) { val action = actions.first() activeAction.postValue(action) } @@ -39,6 +40,7 @@ class ActionSelectViewModel(private val application: Application) : ViewModel() action.requiredParams.forEach { if (!isStandardVariable(it)) variableMap[it] = "" } + nonStandardVariables.postValue(variableMap) } @@ -47,9 +49,8 @@ class ActionSelectViewModel(private val application: Application) : ViewModel() } fun updateNonStandardVariables(key: String, value: String) { - var map = nonStandardVariables.value - if (map == null) map = linkedMapOf() + val map = nonStandardVariables.value ?: linkedMapOf() map[key] = value - nonStandardVariables.postValue(map!!) + nonStandardVariables.postValue(map) } } \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/bonus/Bonus.kt b/app/src/main/java/com/hover/stax/bonus/Bonus.kt new file mode 100644 index 000000000..adef25845 --- /dev/null +++ b/app/src/main/java/com/hover/stax/bonus/Bonus.kt @@ -0,0 +1,26 @@ +package com.hover.stax.bonus + +import androidx.room.ColumnInfo +import androidx.room.Entity + +@Entity( + tableName = "bonuses", + primaryKeys = ["user_channel", "purchase_channel"] +) +data class Bonus( + + @ColumnInfo(name = "user_channel") + val userChannel: Int, + + @ColumnInfo(name = "purchase_channel") + val purchaseChannel: Int, + + @ColumnInfo(name = "bonus_percent") + val bonusPercent: Double, + + val message: String +) { + override fun toString(): String { + return message + } +} \ 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/bonus/BonusDao.kt new file mode 100644 index 000000000..25e0ea439 --- /dev/null +++ b/app/src/main/java/com/hover/stax/bonus/BonusDao.kt @@ -0,0 +1,41 @@ +package com.hover.stax.bonus + +import androidx.room.* +import kotlinx.coroutines.flow.Flow + +@Dao +interface BonusDao { + + @get:Query("SELECT * FROM bonuses") + val bonuses: Flow> + + @Query("SELECT * FROM bonuses WHERE purchase_channel = :purchaseChannelId") + fun getBonusByPurchaseChannel(purchaseChannelId: Int): Bonus? + + @Query("SELECT * FROM bonuses WHERE purchase_channel IN (:purchaseChannelIds) AND user_channel in (:userChannelIds)") + fun getBonuses(purchaseChannelIds: List, userChannelIds: List): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(bonus: Bonus) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAll(bonuses: List) + + @Update(onConflict = OnConflictStrategy.REPLACE) + fun update(bonus: Bonus) + + @Update + fun updateAll(bonuses: List) + + @Delete + fun delete(bonus: Bonus) + + @Query("DELETE FROM bonuses") + fun deleteAll() + + @Transaction + fun deleteAndSave(bonuses: List) { + deleteAll() + insertAll(bonuses) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/bonus/BonusRepo.kt b/app/src/main/java/com/hover/stax/bonus/BonusRepo.kt new file mode 100644 index 000000000..28f1e60fc --- /dev/null +++ b/app/src/main/java/com/hover/stax/bonus/BonusRepo.kt @@ -0,0 +1,24 @@ +package com.hover.stax.bonus + +import com.hover.stax.database.AppDatabase + +class BonusRepo(val db: AppDatabase) { + + private val dao = db.bonusDao() + + val bonuses = dao.bonuses + + fun save(bonus: Bonus) = dao.insert(bonus) + + fun save(bonuses: List) = dao.insertAll(bonuses) + + fun getBonusByPurchaseChannel(purchaseChannelId: Int): Bonus? = dao.getBonusByPurchaseChannel(purchaseChannelId) + + fun getBonuses(purchaseChannelIds: List, userChannelIds: List): List = dao.getBonuses(purchaseChannelIds, userChannelIds) + + fun delete(bonus: Bonus) = dao.delete(bonus) + + fun delete() = dao.deleteAll() + + fun updateBonuses(bonuses: List) = dao.deleteAndSave(bonuses) +} \ 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 new file mode 100644 index 000000000..a27521909 --- /dev/null +++ b/app/src/main/java/com/hover/stax/bonus/BonusViewModel.kt @@ -0,0 +1,85 @@ +package com.hover.stax.bonus + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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.database.DatabaseRepo +import com.hover.stax.utils.toHni +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import timber.log.Timber + +class BonusViewModel(val repo: BonusRepo, private val dbRepo: DatabaseRepo) : ViewModel() { + + private val bonusList = MutableLiveData>(emptyList()) + private val db = Firebase.firestore + private val settings = firestoreSettings { isPersistenceEnabled = true } + + val bonuses: LiveData> get() = bonusList + + init { + db.firestoreSettings = settings + fetchBonuses() + } + + private fun fetchBonuses() { + db.collection("bonuses") + .get() + .addOnSuccessListener { snapshot -> + val results = snapshot.map { document -> + Bonus( + document.data["user_channel"].toString().toInt(), document.data["purchase_channel"].toString().toInt(), + document.data["bonus_percent"].toString().toDouble(), document.data["message"].toString() + ) + } + + saveBonuses(results) + } + .addOnFailureListener { + Timber.e("Error fetching bonuses: ${it.localizedMessage}") + bonusList.postValue(emptyList()) + } + } + + private fun saveBonuses(bonuses: List) = viewModelScope.launch(Dispatchers.IO) { + repo.updateBonuses(bonuses.filter { dbRepo.getChannel(it.purchaseChannel) != null }) + } + + fun getBonuses() = viewModelScope.launch(Dispatchers.IO) { + repo.bonuses.collect { + checkIfEligible(it) + } + } + + fun getBonusByChannelId(channelId: Int): Bonus? = repo.getBonusByPurchaseChannel(channelId) + + private fun checkIfEligible(bonusItems: List) = viewModelScope.launch(Dispatchers.IO) { + val simHnis = dbRepo.presentSims.map { it.osReportedHni } + val bonusChannels = dbRepo.getChannelsByIds(bonusItems.map { it.purchaseChannel }) + + val showBonuses = hasValidSim(simHnis, bonusChannels) + bonusList.postValue(if (showBonuses) bonusItems else emptyList()) + } + + /** + * Extract the hnis from the bonus channels and compare with current sim hnis. + * Return true if user has a valid sim + */ + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/bounties/BountyListItem.kt b/app/src/main/java/com/hover/stax/bounties/BountyListItem.kt index 10d2a0acb..5c30919fb 100644 --- a/app/src/main/java/com/hover/stax/bounties/BountyListItem.kt +++ b/app/src/main/java/com/hover/stax/bounties/BountyListItem.kt @@ -43,10 +43,10 @@ class BountyListItem(context: Context, attrs: AttributeSet?) : LinearLayout(cont 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_info_red, false, navTransactionDetail()) + 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_info_red, true, showBountyDetail()) + 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) diff --git a/app/src/main/java/com/hover/stax/channels/AddChannelsFragment.kt b/app/src/main/java/com/hover/stax/channels/AddChannelsFragment.kt index 405366657..2d777c40b 100644 --- a/app/src/main/java/com/hover/stax/channels/AddChannelsFragment.kt +++ b/app/src/main/java/com/hover/stax/channels/AddChannelsFragment.kt @@ -11,6 +11,7 @@ import android.view.View.VISIBLE import android.view.ViewGroup import androidx.core.text.HtmlCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.lifecycle.Observer import androidx.recyclerview.selection.SelectionPredicates import androidx.recyclerview.selection.SelectionTracker @@ -19,6 +20,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.WorkManager import com.hover.stax.R import com.hover.stax.balances.BalancesViewModel +import com.hover.stax.bonus.BonusViewModel import com.hover.stax.databinding.FragmentAddChannelsBinding import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.Constants @@ -27,6 +29,10 @@ import com.hover.stax.utils.Utils import com.hover.stax.views.RequestServiceDialog import com.hover.stax.views.StaxDialog +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.internal.filterList import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber @@ -36,6 +42,7 @@ class AddChannelsFragment : Fragment(), ChannelsAdapter.SelectListener { private val channelsViewModel: ChannelsViewModel by viewModel() private val balancesViewModel: BalancesViewModel by sharedViewModel() + private val bonusViewModel: BonusViewModel by sharedViewModel() private var _binding: FragmentAddChannelsBinding? = null private val binding get() = _binding!! @@ -150,11 +157,22 @@ class AddChannelsFragment : Fragment(), ChannelsAdapter.SelectListener { binding.channelsListCard.hideProgressIndicator() if (channels.isNotEmpty()) { - Timber.e("Here to add ${channels.size}") - updateAdapter(Channel.sort(channels, false)) - binding.emptyState.root.visibility = GONE - binding.channelsList.visibility = VISIBLE - binding.errorText.visibility = GONE + lifecycleScope.launch { + val bonusChannelIds = bonusViewModel.bonuses.value?.map { it.purchaseChannel } + + val list = if(!bonusChannelIds.isNullOrEmpty()) + channels.filterNot { bonusChannelIds.contains(it.id) } + else + channels + + updateAdapter(list.filterNot { it.selected }) + + withContext(Dispatchers.Main) { + binding.emptyState.root.visibility = GONE + binding.channelsList.visibility = VISIBLE + binding.errorText.visibility = GONE + } + } } else if(channelsViewModel.isInSearchMode()) showEmptyState() else setError(R.string.channels_error_nodata) @@ -189,10 +207,8 @@ class AddChannelsFragment : Fragment(), ChannelsAdapter.SelectListener { else { binding.errorText.visibility = GONE - val selectedChannels = mutableListOf() - tracker.selection.forEach { selection -> - selectedChannels.addAll(selectAdapter.currentList.filter { it.id.toLong() == selection }) - } + val ids = tracker.selection + val selectedChannels = selectAdapter.currentList.filter { ids.contains(it.id.toLong()) } channelsViewModel.setChannelsSelected(selectedChannels) channelsViewModel.createAccounts(selectedChannels) diff --git a/app/src/main/java/com/hover/stax/channels/ChannelsViewHolder.kt b/app/src/main/java/com/hover/stax/channels/ChannelsViewHolder.kt index 7869dffd4..acce5c4cd 100644 --- a/app/src/main/java/com/hover/stax/channels/ChannelsViewHolder.kt +++ b/app/src/main/java/com/hover/stax/channels/ChannelsViewHolder.kt @@ -9,7 +9,7 @@ import androidx.recyclerview.selection.ItemDetailsLookup import androidx.recyclerview.widget.RecyclerView import com.google.android.material.checkbox.MaterialCheckBox import com.hover.stax.databinding.StaxSpinnerItemWithLogoBinding -import com.hover.stax.utils.UIHelper +import com.hover.stax.utils.UIHelper.loadImage class ChannelsViewHolder(val binding: StaxSpinnerItemWithLogoBinding) : RecyclerView.ViewHolder(binding.root) { @@ -27,7 +27,7 @@ class ChannelsViewHolder(val binding: StaxSpinnerItemWithLogoBinding) : Recycler id.text = channel.id.toString() channelText.text = channel.toString() - UIHelper.loadImage(binding.root.context, channel.logoUrl, logo) + logo.loadImage(binding.root.context, channel.logoUrl) } fun getItemDetails(): ItemDetailsLookup.ItemDetails = object : ItemDetailsLookup.ItemDetails() { diff --git a/app/src/main/java/com/hover/stax/channels/ChannelsViewModel.kt b/app/src/main/java/com/hover/stax/channels/ChannelsViewModel.kt index 4049c02c4..249eda62c 100644 --- a/app/src/main/java/com/hover/stax/channels/ChannelsViewModel.kt +++ b/app/src/main/java/com/hover/stax/channels/ChannelsViewModel.kt @@ -15,6 +15,7 @@ import com.hover.sdk.sims.SimInfo import com.hover.stax.R import com.hover.stax.accounts.Account import com.hover.stax.accounts.AccountDropdown +import com.hover.stax.accounts.ChannelWithAccounts import com.hover.stax.database.DatabaseRepo import com.hover.stax.notifications.PushNotificationTopicsInterface import com.hover.stax.schedules.Schedule @@ -84,11 +85,13 @@ class ChannelsViewModel(val application: Application, val repo: DatabaseRepo) : } } - private fun filterChannels(channels: List?) { - if(!channels.isNullOrEmpty()) { + private fun filterChannels(channels: List) { + if (channels.isNotEmpty()) { viewModelScope.launch(Dispatchers.IO) { - val filteredList = channels.filter { it.toString().toFilteringStandard() - .contains(filterQuery.value!!.toFilteringStandard()) } + val filteredList = channels.filter { + it.toString().toFilteringStandard() + .contains(filterQuery.value!!.toFilteringStandard()) + } filteredChannels.postValue(filteredList) } } @@ -96,11 +99,11 @@ class ChannelsViewModel(val application: Application, val repo: DatabaseRepo) : fun runChannelFilter(value: String) { filterQuery.value = value - val listToFilter = if(!simChannels.value.isNullOrEmpty()) simChannels.value!! else allChannels.value - filterChannels(listToFilter) + val listToFilter: List? = if (!simChannels.value.isNullOrEmpty()) simChannels.value else allChannels.value + filterChannels(listToFilter!!) } - fun isInSearchMode() : Boolean { + fun isInSearchMode(): Boolean { return !filterQuery.value!!.isAbsolutelyEmpty() } @@ -193,7 +196,7 @@ class ChannelsViewModel(val application: Application, val repo: DatabaseRepo) : } private fun setActiveChannelIfNull(channels: List) { - if (!channels.isNullOrEmpty() && activeChannel.value == null) + if (channels.isNotEmpty() && activeChannel.value == null) activeChannel.value = channels.firstOrNull { it.defaultAccount } } @@ -202,20 +205,44 @@ class ChannelsViewModel(val application: Application, val repo: DatabaseRepo) : } fun setActiveAccount(account: Account?) { - Timber.e("Active account at the moment is $account") account?.let { activeAccount.postValue(it) } } private fun setActiveChannel(channelId: Int, accountId: Int) { - val channelAccounts = repo.getChannelAndAccounts(channelId) - channelAccounts?.let { - Timber.e("Active channel ${it.channel}") - activeChannel.postValue(it.channel) - + val txnChannelAccounts = repo.getChannelAndAccounts(channelId) + + /* Handles instances where a channel without accounts e.g Stax Airtime was used. + ** Fetches the channel from the account instead. Active channel remains as was in transaction. + */ + val c = if (txnChannelAccounts != null && txnChannelAccounts.accounts.isEmpty()) { + val a = repo.getAccount(accountId) + repo.getChannelAndAccounts(a!!.channelId) + } else + txnChannelAccounts + + c?.let { + activeChannel.postValue(txnChannelAccounts?.channel ?: it.channel) setActiveAccount(it.accounts.firstOrNull { account -> account.id == accountId }) } } + fun setActiveChannelAndAccount(channelId: Int, accountChannelId: Int) = viewModelScope.launch(Dispatchers.IO) { + val channel = repo.getChannel(channelId) + channel?.let { activeChannel.postValue(it) } ?: run { Timber.e("Airtime channel with id $channelId not found") } + + val accounts = repo.getAccounts(accountChannelId) + if (accounts.isNotEmpty()) { + setActiveAccount(accounts.firstOrNull()) + } else { + repo.getChannel(accountChannelId)?.let { + setChannelsSelected(listOf(it)) + createAccounts(listOf(it)) + } + + setActiveAccount(repo.getAccounts(accountChannelId).firstOrNull()) + } + } + private fun loadActions(t: String?) { if (t == null) return @@ -229,7 +256,7 @@ class ChannelsViewModel(val application: Application, val repo: DatabaseRepo) : } private fun loadActions(channels: List) { - if (channels.isNullOrEmpty()) return + if (channels.isEmpty()) return if (type.value == HoverAction.BALANCE) loadActions(channels, type.value!!) @@ -238,7 +265,8 @@ class ChannelsViewModel(val application: Application, val repo: DatabaseRepo) : private fun loadActions(channel: Channel, t: String) = viewModelScope.launch(Dispatchers.IO) { channelActions.postValue( if (t == HoverAction.P2P) repo.getTransferActions(channel.id) - else repo.getActions(channel.id, t)) + else repo.getActions(channel.id, t) + ) } private fun loadAccounts() = viewModelScope.launch { @@ -253,18 +281,16 @@ class ChannelsViewModel(val application: Application, val repo: DatabaseRepo) : } } - fun setChannelsSelected(channels: List?) { - viewModelScope.launch(Dispatchers.IO) { - if (channels.isNullOrEmpty()) return@launch + fun setChannelsSelected(channels: List?) = viewModelScope.launch(Dispatchers.IO) { + if (channels.isNullOrEmpty()) return@launch - channels.forEachIndexed { index, channel -> - logChoice(channel) - channel.selected = true - channel.defaultAccount = selectedChannels.value.isNullOrEmpty() && index == 0 - repo.update(channel) + channels.forEachIndexed { index, channel -> + logChoice(channel) + channel.selected = true + channel.defaultAccount = selectedChannels.value.isNullOrEmpty() && index == 0 + repo.update(channel) - ActionApi.scheduleActionConfigUpdate(channel.countryAlpha2, 24, application) - } + ActionApi.scheduleActionConfigUpdate(channel.countryAlpha2, 24, application) } } @@ -320,6 +346,8 @@ class ChannelsViewModel(val application: Application, val repo: DatabaseRepo) : fun getChannel(channelId: Int): Channel? = repo.getChannel(channelId) + fun getChannelAndAccounts(channelId: Int): ChannelWithAccounts? = repo.getChannelAndAccounts(channelId) + fun createAccounts(channels: List) = viewModelScope.launch(Dispatchers.IO) { val defaultAccount = repo.getDefaultAccount() @@ -327,18 +355,12 @@ class ChannelsViewModel(val application: Application, val repo: DatabaseRepo) : with(it) { val accountName: String = if (getFetchAccountAction(it.id) == null) name else Constants.PLACEHOLDER //placeholder alias for easier identification later val account = Account(accountName, name, logoUrl, accountNo, id, primaryColorHex, secondaryColorHex, defaultAccount == null) + repo.insert(account) } } } - @Deprecated(message = "Newer versions of the app don't need this", replaceWith = ReplaceWith(""), level = DeprecationLevel.WARNING) - fun migrateAccounts() = viewModelScope.launch(Dispatchers.IO) { - if (accounts.value.isNullOrEmpty() && !selectedChannels.value.isNullOrEmpty()) { - createAccounts(selectedChannels.value!!) - } - } - fun fetchAccounts(channelId: Int) = viewModelScope.launch(Dispatchers.IO) { val channelAccounts = repo.getAccounts(channelId) accounts.postValue(channelAccounts) @@ -346,7 +368,7 @@ class ChannelsViewModel(val application: Application, val repo: DatabaseRepo) : private fun updateAccounts() = viewModelScope.launch(Dispatchers.IO) { val savedAccounts = repo.getAllAccounts() - if (savedAccounts.isNullOrEmpty()) return@launch + if (savedAccounts.isEmpty()) return@launch if (savedAccounts.none { it.isDefault }) { val defaultChannel: Channel? = selectedChannels.value?.firstOrNull { it.defaultAccount } @@ -360,9 +382,9 @@ class ChannelsViewModel(val application: Application, val repo: DatabaseRepo) : } } - override fun highlightAccount(account: Account) { + override fun highlightAccount(account: Account, channelOverride: Int) { viewModelScope.launch(Dispatchers.IO) { - val channel = repo.getChannel(account.channelId) + val channel = repo.getChannel(if (channelOverride != 0) channelOverride else account.channelId) setActiveChannel(channel!!) activeAccount.postValue(account) } @@ -372,8 +394,9 @@ class ChannelsViewModel(val application: Application, val repo: DatabaseRepo) : if (!accounts.value.isNullOrEmpty()) { val accts = accounts.value!! //remove current default account - val current: Account? = accts.firstOrNull { it.isDefault } - current?.isDefault = false + val current: Account? = accts.firstOrNull { it.isDefault }.also { + it?.isDefault = false + } repo.update(current) val a = accts.first { it.id == account.id } diff --git a/app/src/main/java/com/hover/stax/contacts/PhoneHelper.java b/app/src/main/java/com/hover/stax/contacts/PhoneHelper.java deleted file mode 100644 index 5f7991c0a..000000000 --- a/app/src/main/java/com/hover/stax/contacts/PhoneHelper.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.hover.stax.contacts; - -import android.util.Log; - -import com.google.i18n.phonenumbers.NumberParseException; -import com.google.i18n.phonenumbers.PhoneNumberUtil; -import com.google.i18n.phonenumbers.Phonenumber; -import com.hover.stax.utils.AnalyticsUtil; - -public class PhoneHelper { - final static private String TAG = "PhoneHelper"; - - public static String normalizeNumberByCountry(String number, String from_country, String to_country) { - String phoneNumber = number; - try { - phoneNumber = convertToCountry(number, from_country, to_country); - Log.e("Contact", "Normalized number: " + phoneNumber); - } catch (NumberParseException e) { - Log.e("Contact", "error formating number", e); - } - return phoneNumber; - } - - private static String convertToCountry(String number, String from_country, String to_country) throws NumberParseException, IllegalStateException { - PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); - try { - Phonenumber.PhoneNumber phone = phoneUtil.parse(number, to_country); -// Most cases we've seen the number format is that used for dialing without the plus - number = phoneUtil.formatNumberForMobileDialing(phone, from_country, false).replace("+", ""); - } catch (IllegalStateException e) { - Log.e(TAG, "Google phone number util failed.", e); - } - return number; - } - - public static String getNationalSignificantNumber(String number, String country) { - try { - PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); - Phonenumber.PhoneNumber phone = phoneUtil.parse(number, country); - return phoneUtil.getNationalSignificantNumber(phone); - } catch (NumberParseException | IllegalStateException e) { - AnalyticsUtil.logErrorAndReportToFirebase(TAG, "Failed to transform number for contact; doing it the old fashioned way.", e); - return number.startsWith("+") ? number.substring(4) : (number.startsWith("0") ? number.substring(1) : number); - } - } - - public static String getInternationalNumber(String country, String phoneNumber) throws NumberParseException, IllegalStateException { - PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); - Phonenumber.PhoneNumber phone = getPhone(country, phoneNumber); - phone.getCountryCode(); - return phoneUtil.format(phone, PhoneNumberUtil.PhoneNumberFormat.E164); - } - - - public static String getInternationalNumberNoPlus(String accountNumber, String country) { - try { - return getInternationalNumber(country, accountNumber).replace("+", ""); - } catch (NumberParseException | IllegalStateException e) { - AnalyticsUtil.logErrorAndReportToFirebase(TAG, "Failed to transform number for contact; doing it the old fashioned way.", e); - return accountNumber.replace("+", ""); - } - } - - private static Phonenumber.PhoneNumber getPhone(String country, String phoneNumber) throws NumberParseException, IllegalStateException { - PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); - return phoneUtil.parse(phoneNumber, country); - } -} diff --git a/app/src/main/java/com/hover/stax/contacts/PhoneHelper.kt b/app/src/main/java/com/hover/stax/contacts/PhoneHelper.kt new file mode 100644 index 000000000..27850c4e6 --- /dev/null +++ b/app/src/main/java/com/hover/stax/contacts/PhoneHelper.kt @@ -0,0 +1,79 @@ +package com.hover.stax.contacts + +import com.google.i18n.phonenumbers.NumberParseException +import com.google.i18n.phonenumbers.PhoneNumberUtil +import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber +import com.hover.stax.utils.AnalyticsUtil.logErrorAndReportToFirebase +import timber.log.Timber + +object PhoneHelper { + + private const val TAG = "PhoneHelper" + + fun normalizeNumberByCountry(number: String, from_country: String, to_country: String): String { + var phoneNumber = number + try { + phoneNumber = convertToCountry(number, from_country, to_country) + Timber.e("Normalized number: $phoneNumber") + } catch (e: NumberParseException) { + Timber.e(e, "error formatting number") + } + + return phoneNumber + } + + @Throws(NumberParseException::class, IllegalStateException::class) + private fun convertToCountry(num: String, from_country: String, to_country: String): String { + var number = num + val phoneUtil = PhoneNumberUtil.getInstance() + try { + val phone = phoneUtil.parse(number, to_country) + // Most cases we've seen the number format is that used for dialing without the plus + number = phoneUtil.formatNumberForMobileDialing(phone, from_country, false).replace("+", "") + } catch (e: IllegalStateException) { + Timber.e(e, "Google phone number util failed.") + } + return number + } + + @JvmStatic + fun getNationalSignificantNumber(number: String, country: String?): String { + return try { + val phoneUtil = PhoneNumberUtil.getInstance() + val phone = phoneUtil.parse(number, country) + phoneUtil.getNationalSignificantNumber(phone) + } catch (e: NumberParseException) { + logErrorAndReportToFirebase(TAG, "Failed to transform number for contact; doing it the old fashioned way.", e) + if (number.startsWith("+")) number.substring(4) else if (number.startsWith("0")) number.substring(1) else number + } catch (e: IllegalStateException) { + logErrorAndReportToFirebase(TAG, "Failed to transform number for contact; doing it the old fashioned way.", e) + if (number.startsWith("+")) number.substring(4) else if (number.startsWith("0")) number.substring(1) else number + } + } + + @Throws(NumberParseException::class, IllegalStateException::class) + fun getInternationalNumber(country: String, phoneNumber: String): String { + val phoneUtil = PhoneNumberUtil.getInstance() + val phone = getPhone(country, phoneNumber) + phone.countryCode + return phoneUtil.format(phone, PhoneNumberUtil.PhoneNumberFormat.E164) + } + + fun getInternationalNumberNoPlus(accountNumber: String, country: String): String { + return try { + getInternationalNumber(country, accountNumber).replace("+", "") + } catch (e: NumberParseException) { + logErrorAndReportToFirebase(TAG, "Failed to transform number for contact; doing it the old fashioned way.", e) + accountNumber.replace("+", "") + } catch (e: IllegalStateException) { + logErrorAndReportToFirebase(TAG, "Failed to transform number for contact; doing it the old fashioned way.", e) + accountNumber.replace("+", "") + } + } + + @Throws(NumberParseException::class, IllegalStateException::class) + private fun getPhone(country: String, phoneNumber: String): PhoneNumber { + val phoneUtil = PhoneNumberUtil.getInstance() + return phoneUtil.parse(phoneNumber, country) + } +} \ No newline at end of file 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 326b39696..4e0be481b 100644 --- a/app/src/main/java/com/hover/stax/database/AppDatabase.kt +++ b/app/src/main/java/com/hover/stax/database/AppDatabase.kt @@ -8,6 +8,8 @@ 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.channels.Channel import com.hover.stax.channels.ChannelDao import com.hover.stax.contacts.ContactDao @@ -27,12 +29,13 @@ import java.util.concurrent.Executors @Database( entities = [ - Channel::class, StaxTransaction::class, StaxContact::class, Request::class, Schedule::class, Account::class, Paybill::class, StaxUser::class + Channel::class, StaxTransaction::class, StaxContact::class, Request::class, Schedule::class, Account::class, Paybill::class, StaxUser::class, Bonus::class ], - version = 38, + version = 39, autoMigrations = [ AutoMigration(from = 36, to = 37), - AutoMigration(from = 37, to = 38) + AutoMigration(from = 37, to = 38), + AutoMigration(from = 38, to = 39) ] ) abstract class AppDatabase : RoomDatabase() { @@ -53,6 +56,8 @@ abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao + abstract fun bonusDao(): BonusDao + companion object { @Volatile diff --git a/app/src/main/java/com/hover/stax/database/DatabaseRepo.kt b/app/src/main/java/com/hover/stax/database/DatabaseRepo.kt index 09e3b5563..c5918fdbb 100644 --- a/app/src/main/java/com/hover/stax/database/DatabaseRepo.kt +++ b/app/src/main/java/com/hover/stax/database/DatabaseRepo.kt @@ -87,7 +87,7 @@ class DatabaseRepo(db: AppDatabase, sdkDb: HoverRoomDatabase) { val presentSims: List get() = simDao.present - fun getSims(hnis: Array?): List { + fun getSims(hnis: Array): List { return simDao.getPresentByHnis(hnis) } diff --git a/app/src/main/java/com/hover/stax/database/Modules.kt b/app/src/main/java/com/hover/stax/database/Modules.kt index 065c35da0..f89cdb9af 100644 --- a/app/src/main/java/com/hover/stax/database/Modules.kt +++ b/app/src/main/java/com/hover/stax/database/Modules.kt @@ -4,6 +4,8 @@ import com.hover.sdk.database.HoverRoomDatabase import com.hover.stax.accounts.AccountDetailViewModel import com.hover.stax.actions.ActionSelectViewModel 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.ChannelsViewModel import com.hover.stax.faq.FaqViewModel @@ -39,13 +41,14 @@ val appModule = module { viewModel { BannerViewModel(get(), get()) } viewModel { FutureViewModel(get()) } viewModel { LoginViewModel(get(), get(), get(), get()) } - viewModel { TransactionDetailsViewModel(get(), get()) } + viewModel { TransactionDetailsViewModel(get(), get(), get()) } viewModel { LibraryViewModel(get(), get()) } viewModel { LanguageViewModel(get()) } viewModel { BountyViewModel(get(), get()) } viewModel { FinancialTipsViewModel() } viewModel { PaybillViewModel(get(), get(), get()) } viewModel { RequestDetailViewModel(get()) } + viewModel { BonusViewModel(get(), get()) } } val dataModule = module(createdAtStart = true) { @@ -55,6 +58,7 @@ val dataModule = module(createdAtStart = true) { single { DatabaseRepo(get(), get()) } single { PaybillRepo(get()) } single { UserRepo(get()) } + single { BonusRepo(get()) } } val networkModule = module { diff --git a/app/src/main/java/com/hover/stax/home/AbstractSDKCaller.kt b/app/src/main/java/com/hover/stax/home/AbstractSDKCaller.kt index b5f56f0d3..3888eb553 100644 --- a/app/src/main/java/com/hover/stax/home/AbstractSDKCaller.kt +++ b/app/src/main/java/com/hover/stax/home/AbstractSDKCaller.kt @@ -110,7 +110,8 @@ abstract class AbstractSDKCaller : AbstractGoogleAuthActivity(), PushNotificatio } selectedAccount?.run { hsb.setAccountId(id.toString()) } - transferViewModel.contact.value?.let { addRecipientInfo(hsb) } + transferViewModel.contact.value?.let { + addRecipientInfo(hsb) } } runAction(hsb) diff --git a/app/src/main/java/com/hover/stax/home/HomeFragment.kt b/app/src/main/java/com/hover/stax/home/HomeFragment.kt index 6d33d55c8..3e03b8f56 100644 --- a/app/src/main/java/com/hover/stax/home/HomeFragment.kt +++ b/app/src/main/java/com/hover/stax/home/HomeFragment.kt @@ -5,19 +5,26 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDirections import androidx.navigation.fragment.findNavController import com.hover.sdk.actions.HoverAction import com.hover.stax.R +import com.hover.stax.bonus.Bonus +import com.hover.stax.bonus.BonusViewModel +import com.hover.stax.channels.ChannelsViewModel import com.hover.stax.databinding.FragmentHomeBinding import com.hover.stax.financialTips.FinancialTip import com.hover.stax.financialTips.FinancialTipsViewModel -import com.hover.stax.inapp_banner.BannerViewModel import com.hover.stax.utils.AnalyticsUtil import com.hover.stax.utils.Constants import com.hover.stax.utils.NavUtil import com.hover.stax.utils.Utils import com.hover.stax.utils.network.NetworkMonitor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber @@ -27,8 +34,9 @@ class HomeFragment : Fragment() { private var _binding: FragmentHomeBinding? = null private val binding get() = _binding!! - private val bannerViewModel: BannerViewModel by viewModel() 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()) @@ -52,24 +60,25 @@ class HomeFragment : Fragment() { setUpWellnessTips() } - private fun getTransferDirection(type: String) : NavDirections { - return HomeFragmentDirections.actionNavigationHomeToNavigationTransfer(type) + private fun getTransferDirection(type: String, channelId: Int = 0): NavDirections { + return HomeFragmentDirections.actionNavigationHomeToNavigationTransfer(type).setChannelId(channelId) } - private fun setupBanner() { - with(bannerViewModel) { - qualifiedBanner.observe(viewLifecycleOwner) { banner -> - if (banner != null) { - AnalyticsUtil.logAnalyticsEvent(getString(R.string.displaying_in_app_banner, banner.id), requireContext()) - binding.homeBanner.visibility = View.VISIBLE - binding.homeBanner.display(banner) - - binding.homeBanner.setOnClickListener { - AnalyticsUtil.logAnalyticsEvent(getString(R.string.clicked_on_banner), requireContext()) - Utils.openUrl(banner.url, requireActivity()) - closeCampaign(banner.id) + + private fun setupBanner() = with(bonusViewModel) { + bonuses.observe(viewLifecycleOwner) { b -> + if (b.isNotEmpty()) { + with(binding.bonusCard) { + message.text = b.first().message + } + binding.bonusCard.apply { + cardBonus.visibility = View.VISIBLE + cta.setOnClickListener { + channelsViewModel // viewmodel must be instantiated in the main thread before it can be accessible on other threads + AnalyticsUtil.logAnalyticsEvent(getString(R.string.clicked_bonus_airtime_banner), requireActivity()) + validateTransferAction(b.first()) } - } else binding.homeBanner.visibility = View.GONE - } + } + } else binding.bonusCard.cardBonus.visibility = View.GONE } } @@ -129,6 +138,22 @@ class HomeFragment : Fragment() { } } + private fun validateTransferAction(bonus: Bonus) = lifecycleScope.launch(Dispatchers.IO) { + val channelAccounts = channelsViewModel.getChannelAndAccounts(bonus.userChannel) + + if(channelAccounts != null && channelAccounts.accounts.isEmpty()) { + val channels = listOf(channelAccounts.channel) + channelsViewModel.apply { + setChannelsSelected(channels) + createAccounts(channels) + } + } + + withContext(Dispatchers.Main) { + navigateTo(getTransferDirection(HoverAction.AIRTIME, bonus.purchaseChannel)) + } + } + override fun onDestroyView() { super.onDestroyView() _binding = null 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 67c7303ef..d9b1c13a9 100644 --- a/app/src/main/java/com/hover/stax/home/MainActivity.kt +++ b/app/src/main/java/com/hover/stax/home/MainActivity.kt @@ -14,6 +14,7 @@ import com.hover.stax.accounts.DUMMY import com.hover.stax.actions.ActionSelectViewModel import com.hover.stax.balances.BalanceAdapter import com.hover.stax.balances.BalancesViewModel +import com.hover.stax.bonus.BonusViewModel import com.hover.stax.databinding.ActivityMainBinding import com.hover.stax.financialTips.FinancialTipsFragment import com.hover.stax.login.LoginViewModel @@ -38,6 +39,8 @@ class MainActivity : AbstractRequestActivity(), BalancesViewModel.RunBalanceList private val transferViewModel: TransferViewModel by viewModel() private val historyViewModel: TransactionHistoryViewModel by viewModel() private val loginViewModel: LoginViewModel by viewModel() + private val bonusViewModel: BonusViewModel by viewModel() + private val bountyRequest = 3000 private lateinit var binding: ActivityMainBinding @@ -59,6 +62,8 @@ class MainActivity : AbstractRequestActivity(), BalancesViewModel.RunBalanceList checkForFragmentDirection(intent) observeForAppReview() setGoogleLoginInterface(this) + + bonusViewModel.getBonuses() } override fun onNewIntent(intent: Intent?) { 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 b8eb35d18..aa18a1979 100644 --- a/app/src/main/java/com/hover/stax/login/AbstractGoogleAuthActivity.kt +++ b/app/src/main/java/com/hover/stax/login/AbstractGoogleAuthActivity.kt @@ -1,7 +1,6 @@ package com.hover.stax.login import android.content.Intent -import android.os.Build import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat @@ -34,24 +33,25 @@ abstract class AbstractGoogleAuthActivity : AppCompatActivity() { setLoginObserver() updateManager = AppUpdateManagerFactory.create(this) - checkForUpdates() + + if (!BuildConfig.DEBUG) + checkForUpdates() } //checks that the update has not stalled override fun onResume() { super.onResume() - if(!BuildConfig.DEBUG) { + if (!BuildConfig.DEBUG) updateManager.appUpdateInfo.addOnSuccessListener { updateInfo -> //if the update is downloaded but not installed, notify user to complete the update if (updateInfo.installStatus() == InstallStatus.DOWNLOADED) showSnackbarForCompleteUpdate() //if an in-app update is already running, resume the update - if(updateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) { + if (updateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) { updateManager.startUpdateFlowForResult(updateInfo, AppUpdateType.IMMEDIATE, this, UPDATE_REQUEST_CODE) } } - } } fun setGoogleLoginInterface(staxGoogleLoginInterface: StaxGoogleLoginInterface) { @@ -79,16 +79,19 @@ abstract class AbstractGoogleAuthActivity : AppCompatActivity() { fun signIn() = startActivityForResult(loginViewModel.signInClient.signInIntent, LOGIN_REQUEST) private fun checkForUpdates() { - if(BuildConfig.DEBUG) { + if (BuildConfig.DEBUG) { val updateInfoTask = updateManager.appUpdateInfo updateInfoTask.addOnSuccessListener { updateInfo -> val updateType = if ((updateInfo.clientVersionStalenessDays() - ?: -1) <= DAYS_FOR_FLEXIBLE_UPDATE) AppUpdateType.FLEXIBLE + ?: -1) <= DAYS_FOR_FLEXIBLE_UPDATE + ) AppUpdateType.FLEXIBLE else AppUpdateType.IMMEDIATE if (updateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && updateInfo.isUpdateTypeAllowed( - updateType)) requestUpdate(updateInfo, updateType) + updateType + ) + ) requestUpdate(updateInfo, updateType) else Timber.i("No new update available") } } @@ -101,7 +104,7 @@ abstract class AbstractGoogleAuthActivity : AppCompatActivity() { showSnackbarForCompleteUpdate() } updateManager.registerListener(installListener!!) - } + } updateManager.startUpdateFlowForResult(updateInfo, updateType, this, UPDATE_REQUEST_CODE) } diff --git a/app/src/main/java/com/hover/stax/paybill/PaybillActionsAdapter.kt b/app/src/main/java/com/hover/stax/paybill/PaybillActionsAdapter.kt index b13965bd6..3b3c6cea0 100644 --- a/app/src/main/java/com/hover/stax/paybill/PaybillActionsAdapter.kt +++ b/app/src/main/java/com/hover/stax/paybill/PaybillActionsAdapter.kt @@ -7,6 +7,7 @@ import com.hover.sdk.actions.HoverAction import com.hover.stax.R import com.hover.stax.databinding.ItemPaybillActionBinding import com.hover.stax.utils.UIHelper +import com.hover.stax.utils.UIHelper.loadImage class PaybillActionsAdapter(private val paybillActions: List, private val clickListener: PaybillActionsClickListener) : RecyclerView.Adapter() { @@ -32,7 +33,7 @@ class PaybillActionsAdapter(private val paybillActions: List, priva binding.root.setOnClickListener { clickListener.onSelectPaybill(action) } - UIHelper.loadImage(binding.root.context, binding.billIcon.context.getString(R.string.root_url).plus(action.to_institution_logo), binding.billIcon) + binding.billIcon.loadImage(binding.root.context, binding.billIcon.context.getString(R.string.root_url).plus(action.to_institution_logo)) } } diff --git a/app/src/main/java/com/hover/stax/paybill/PaybillAdapter.kt b/app/src/main/java/com/hover/stax/paybill/PaybillAdapter.kt index 0beaf189b..ed3f96ed1 100644 --- a/app/src/main/java/com/hover/stax/paybill/PaybillAdapter.kt +++ b/app/src/main/java/com/hover/stax/paybill/PaybillAdapter.kt @@ -9,6 +9,7 @@ import com.hover.stax.R import com.hover.stax.databinding.ItemPaybillSavedBinding import com.hover.stax.utils.GlideApp import com.hover.stax.utils.UIHelper +import com.hover.stax.utils.UIHelper.loadImage class PaybillAdapter(private val paybills: List, private val clickListener: ClickListener) : RecyclerView.Adapter() { @@ -38,7 +39,7 @@ class PaybillAdapter(private val paybills: List, private val clickListe binding.iconLayout.visibility = View.GONE binding.billLogo.visibility = View.VISIBLE - UIHelper.loadImage(binding.root.context, paybill.logoUrl, binding.billLogo) + binding.billLogo.loadImage(binding.root.context, paybill.logoUrl) } binding.root.setOnClickListener { clickListener.onSelectPaybill(paybill) } diff --git a/app/src/main/java/com/hover/stax/transactions/StaxTransaction.java b/app/src/main/java/com/hover/stax/transactions/StaxTransaction.java index d834d3e4e..b7566ea77 100644 --- a/app/src/main/java/com/hover/stax/transactions/StaxTransaction.java +++ b/app/src/main/java/com/hover/stax/transactions/StaxTransaction.java @@ -199,9 +199,18 @@ public String generateLongDescription(HoverAction action, StaxContact contact, C public TransactionStatus getFullStatus() { return new TransactionStatus(this); } - public Boolean isFailed(){ return status.equals(Transaction.FAILED); } - public Boolean isSuccessful() {return status.equals(Transaction.SUCCEEDED);} - public Boolean isBalanceType() {return transaction_type.equals(HoverAction.BALANCE);} + + public Boolean isFailed() { + return status.equals(Transaction.FAILED); + } + + public Boolean isSuccessful() { + return status.equals(Transaction.SUCCEEDED); + } + + public Boolean isBalanceType() { + return transaction_type.equals(HoverAction.BALANCE); + } public boolean isRecorded() { return environment == HoverParameters.MANUAL_ENV; @@ -219,10 +228,9 @@ else if (isRecorded()) } public String getDisplayBalance() { - if(!balance.isEmpty()) { + if (!balance.isEmpty()) { return Utils.formatAmount(balance); - } - else return balance; + } else return balance; } @NotNull diff --git a/app/src/main/java/com/hover/stax/transactions/TransactionDetailsFragment.kt b/app/src/main/java/com/hover/stax/transactions/TransactionDetailsFragment.kt index 47d6a7327..b9c98ca1e 100644 --- a/app/src/main/java/com/hover/stax/transactions/TransactionDetailsFragment.kt +++ b/app/src/main/java/com/hover/stax/transactions/TransactionDetailsFragment.kt @@ -8,6 +8,7 @@ import android.view.View import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup +import android.widget.Button import android.widget.TextView import androidx.core.text.HtmlCompat import androidx.fragment.app.Fragment @@ -26,7 +27,7 @@ import com.hover.stax.utils.AnalyticsUtil.logAnalyticsEvent import com.hover.stax.utils.AnalyticsUtil.logErrorAndReportToFirebase import com.hover.stax.utils.DateUtils import com.hover.stax.utils.NavUtil -import com.hover.stax.utils.UIHelper +import com.hover.stax.utils.UIHelper.loadImage import com.hover.stax.utils.Utils import org.json.JSONException import org.json.JSONObject @@ -72,21 +73,19 @@ class TransactionDetailsFragment : Fragment() { with(binding.infoCard.detailsServiceId.content) { setOnClickListener { Utils.copyToClipboard(this.text.toString(), requireContext()) } } } - private fun showUSSDLog() { - val log = USSDLogBottomSheetFragment.newInstance(args.uuid) - log.show(childFragManager, USSDLogBottomSheetFragment::class.java.simpleName) - } + private fun showUSSDLog() = USSDLogBottomSheetFragment.newInstance(args.uuid).show(childFragManager, USSDLogBottomSheetFragment::class.java.simpleName) private fun startObservers() = with(viewModel) { transaction.observe(viewLifecycleOwner) { showTransaction(it) } action.observe(viewLifecycleOwner) { it?.let { showActionDetails(it) } } contact.observe(viewLifecycleOwner) { updateRecipient(it) } + bonusAmt.observe(viewLifecycleOwner) { showBonusAmount(it) } } - private fun setupContactSupportButton(id: String, contactSupportTextView: TextView) { - contactSupportTextView.setText(R.string.email_support) - contactSupportTextView.setOnClickListener { - resetTryAgainCounter(id) + private fun setupContactSupportButton(id: String, contactSupportTextView: TextView) = contactSupportTextView.apply { + text = getString(R.string.email_support) + setOnClickListener { + resetRetryCounter(id) val deviceId = Hover.getDeviceId(requireContext()) val subject = "Stax Transaction failure - support id- {${deviceId}}" Utils.openEmail(subject, requireActivity()) @@ -94,11 +93,11 @@ class TransactionDetailsFragment : Fragment() { } private fun updateRetryCounter(id: String) { - val currentCount: Int = if (retryCounter[id] != null) retryCounter[id]!! else 0 + val currentCount: Int = retryCounter[id] ?: 0 retryCounter[id] = currentCount + 1 } - private fun resetTryAgainCounter(id: String) { + private fun resetRetryCounter(id: String) { retryCounter[id] = 0 } @@ -114,23 +113,18 @@ class TransactionDetailsFragment : Fragment() { setupContactSupportButton(transaction.action_id, button) else retryTransactionClicked(transaction, button) - } else binding.secondaryStatus.transactionRetryButtonLayoutId.visibility = GONE + } else binding.secondaryStatus.btnRetryTransaction.visibility = GONE + updateDetails(transaction) + } } - private fun showButtonToClick(): TextView { - val transactionButtonsLayout = binding.secondaryStatus.transactionRetryButtonLayoutId - val retryButton = binding.secondaryStatus.btnRetryTransaction - transactionButtonsLayout.visibility = VISIBLE - return retryButton - } + private fun showButtonToClick(): Button = binding.secondaryStatus.btnRetryTransaction.also { it.visibility = VISIBLE } private fun setupRetryBountyButton() { - val bountyButtonsLayout = binding.secondaryStatus.transactionRetryButtonLayoutId - val retryButton = binding.secondaryStatus.btnRetryTransaction - bountyButtonsLayout.visibility = VISIBLE - retryButton.setOnClickListener { retryBountyClicked() } + val retryBtn = showButtonToClick() + retryBtn.setOnClickListener { retryBountyClicked() } } private fun retryBountyClicked() { @@ -143,7 +137,12 @@ class TransactionDetailsFragment : Fragment() { retryButton.setOnClickListener { updateRetryCounter(transaction.action_id) if (transaction.isBalanceType) (requireActivity() as MainActivity).reBuildHoverSession(transaction) - else (requireActivity() as MainActivity).navigateTransferAutoFill(transaction.transaction_type, transaction.uuid) + else + NavUtil.navigate(findNavController(), + TransactionDetailsFragmentDirections.actionTransactionDetailsFragmentToNavigationTransfer(transaction.transaction_type).also { + it.transactionUUID = transaction.uuid + it.channelId = transaction.channel_id + }) } } @@ -156,7 +155,7 @@ class TransactionDetailsFragment : Fragment() { val title = when (transaction.transaction_type) { HoverAction.P2P -> getString(R.string.send_money) HoverAction.AIRTIME -> getString(R.string.buy_airtime) - else -> transaction.transaction_type + else -> transaction.transaction_type.replace("_", " ") } binding.transactionDetailsCard.setTitle(title) @@ -240,7 +239,7 @@ class TransactionDetailsFragment : Fragment() { movementMethod = LinkMovementMethod.getInstance() } if (transaction.isFailed) action?.let { - UIHelper.loadImage(this@TransactionDetailsFragment, getString(R.string.root_url).plus(it.from_institution_logo), binding.secondaryStatus.statusIcon) + binding.secondaryStatus.statusIcon.loadImage(this@TransactionDetailsFragment, getString(R.string.root_url).plus(it.from_institution_logo)) } else binding.secondaryStatus.statusIcon.visibility = GONE } @@ -253,6 +252,17 @@ class TransactionDetailsFragment : Fragment() { } else binding.infoCard.detailsRecipient.setTitle(getString(R.string.self_choice)) } + private fun showBonusAmount(amount: Int) = with(binding.infoCard) { + val txn = viewModel.transaction.value + + if(amount > 0 && (txn != null && txn.isSuccessful)){ + bonusRow.visibility = VISIBLE + bonusAmount.text = amount.toString() + } else { + bonusRow.visibility = GONE + } + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/java/com/hover/stax/transactions/TransactionDetailsViewModel.kt b/app/src/main/java/com/hover/stax/transactions/TransactionDetailsViewModel.kt index 54c79a879..c39e09f39 100644 --- a/app/src/main/java/com/hover/stax/transactions/TransactionDetailsViewModel.kt +++ b/app/src/main/java/com/hover/stax/transactions/TransactionDetailsViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.* import com.hover.sdk.actions.HoverAction import com.hover.sdk.api.Hover import com.hover.sdk.api.Hover.getSMSMessageByUUID +import com.hover.stax.bonus.BonusRepo import com.hover.stax.contacts.StaxContact import com.hover.stax.database.DatabaseRepo import kotlinx.coroutines.Dispatchers @@ -12,14 +13,16 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.json.JSONArray import timber.log.Timber +import kotlin.math.floor -class TransactionDetailsViewModel(val repo: DatabaseRepo, val application: Application) : ViewModel() { +class TransactionDetailsViewModel(val repo: DatabaseRepo, val application: Application, val bonusRepo: BonusRepo) : ViewModel() { val transaction = MutableLiveData() val messages = MediatorLiveData>() var action: LiveData = MutableLiveData() var contact: LiveData = MutableLiveData() var sms: LiveData> = MutableLiveData() + var bonusAmt = MutableLiveData(0) init { action = Transformations.switchMap(transaction) { getLiveAction(it) } @@ -57,18 +60,32 @@ class TransactionDetailsViewModel(val repo: DatabaseRepo, val application: Appli messages.value = UssdCallResponse.generateConvo(Hover.getTransaction(txn.uuid, application), a) } - private fun loadSms(txn: StaxTransaction): List? { + private fun loadSms(txn: StaxTransaction): List { + getBonusAmount(txn) + val t = Hover.getTransaction(txn.uuid, application) return generateSmsConvo(if (t.smsHits != null && t.smsHits.length() > 0) t.smsHits else t.smsMisses) } private fun generateSmsConvo(smsArr: JSONArray): ArrayList { val smses = ArrayList() + for (i in 0 until smsArr.length()) { val sms = getSMSMessageByUUID(smsArr.optString(i), application) Timber.e(sms.uuid) smses.add(UssdCallResponse(null, sms.msg)) } + return smses } + + fun getBonusAmount(staxTransaction: StaxTransaction) = viewModelScope.launch(Dispatchers.IO) { + val bonus = bonusRepo.getBonusByPurchaseChannel(staxTransaction.channel_id) + + if (bonus != null) { + val bonusAmount = floor(bonus.bonusPercent.times(staxTransaction.amount)) + bonusAmt.postValue(bonusAmount.toInt()) + } else + bonusAmt.postValue(0) + } } \ No newline at end of file diff --git a/app/src/main/java/com/hover/stax/transactions/TransactionStatus.kt b/app/src/main/java/com/hover/stax/transactions/TransactionStatus.kt index bd4343f6c..32d0b1767 100644 --- a/app/src/main/java/com/hover/stax/transactions/TransactionStatus.kt +++ b/app/src/main/java/com/hover/stax/transactions/TransactionStatus.kt @@ -9,7 +9,7 @@ class TransactionStatus(val transaction: StaxTransaction) { fun getIcon(): Int { return when (transaction.status) { - Transaction.FAILED -> R.drawable.ic_info_red + Transaction.FAILED -> R.drawable.ic_error Transaction.PENDING -> R.drawable.ic_warning else -> R.drawable.ic_success } 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 6b1d7b744..7e72005fb 100644 --- a/app/src/main/java/com/hover/stax/transfers/AbstractFormFragment.kt +++ b/app/src/main/java/com/hover/stax/transfers/AbstractFormFragment.kt @@ -141,12 +141,14 @@ abstract class AbstractFormFragment : Fragment(), AccountDropdown.AccountFetchLi } override fun fetchAccounts(account: Account) { - dialog = StaxDialog(requireActivity()) - .setDialogTitle(getString(R.string.incomplete_account_setup_header)) - .setDialogMessage(getString(R.string.incomplete_account_setup_desc, account.alias)) - .setPosButton(R.string.check_balance_title) { runBalanceCheck(account.channelId) } - .setNegButton(R.string.btn_cancel, null) - dialog!!.showIt() + if(dialog == null) { + dialog = StaxDialog(requireActivity()) + .setDialogTitle(getString(R.string.incomplete_account_setup_header)) + .setDialogMessage(getString(R.string.incomplete_account_setup_desc, account.alias)) + .setPosButton(R.string.check_balance_title) { runBalanceCheck(account.channelId) } + .setNegButton(R.string.btn_cancel, null) + dialog!!.showIt() + } } private fun runBalanceCheck(channelId: Int) = lifecycleScope.launch(Dispatchers.IO) { 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 0965a41e7..cc590a792 100644 --- a/app/src/main/java/com/hover/stax/transfers/TransferFragment.kt +++ b/app/src/main/java/com/hover/stax/transfers/TransferFragment.kt @@ -7,12 +7,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout +import androidx.lifecycle.lifecycleScope 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.actions.ActionSelect import com.hover.stax.actions.ActionSelectViewModel +import com.hover.stax.bonus.Bonus +import com.hover.stax.bonus.BonusViewModel import com.hover.stax.contacts.ContactInput import com.hover.stax.contacts.StaxContact import com.hover.stax.databinding.FragmentTransferBinding @@ -23,6 +26,8 @@ import com.hover.stax.utils.Utils import com.hover.stax.views.AbstractStatefulInput import com.hover.stax.views.Stax2LineItem import com.hover.stax.views.StaxTextInputLayout +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.getSharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel import timber.log.Timber @@ -31,6 +36,7 @@ import timber.log.Timber class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, NonStandardVariableAdapter.NonStandardVariableInputListener { private val actionSelectViewModel: ActionSelectViewModel by sharedViewModel() + private val bonusViewModel: BonusViewModel by sharedViewModel() private lateinit var transferViewModel: TransferViewModel private val args by navArgs() @@ -43,7 +49,6 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, private var _binding: FragmentTransferBinding? = null private val binding get() = _binding!! - private lateinit var nonStandardSummaryAdapter: NonStandardSummaryAdapter private var nonStandardVariableAdapter: NonStandardVariableAdapter? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -53,7 +58,6 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, setTransactionType(args.transactionType) args.transactionUUID?.let { - Timber.e("TxnUUID is $it. Setting autofill") transferViewModel.autoFill(it) } @@ -76,7 +80,7 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - transferViewModel.reset() //TODO remove if values are removed + transferViewModel.reset() init(binding.root) startObservers(binding.root) @@ -126,6 +130,7 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, observeRecentContacts() observeNonStandardVariables() observeAutoFillToInstitution() + with(transferViewModel) { contact.observe(viewLifecycleOwner) { recipientValue.setContact(it) } request.observe(viewLifecycleOwner) { @@ -151,6 +156,8 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, binding.summaryCard.accountValue.setTitle(it.toString()) } recipientInstitutionSelect.visibility = if (channel != null) View.VISIBLE else View.GONE + + checkForBonus() } } @@ -168,8 +175,15 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, if (it.isEmpty()) setDropdownTouchListener(TransferFragmentDirections.actionNavigationTransferToAccountsFragment()) - if (args.transactionUUID == null) + if (args.channelId != 0) { //to be used with bonus flow. Other uses will require a slight change in this + updateAccountDropdown() + return@observe + } + + if (args.transactionUUID == null) { accountDropdown.setCurrentAccount() + return@observe + } } } @@ -312,7 +326,6 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, } override fun onContactSelected(contact: StaxContact) { -// transferViewModel.setEditing(true) transferViewModel.setContact(contact) contactInput.setSelected(contact) } @@ -322,17 +335,18 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, } private fun updateNonStandardForEntryList(variables: LinkedHashMap) { - val recyclerView = binding.editCard.nonStandardVariableRecyclerView - nonStandardVariableAdapter = NonStandardVariableAdapter(variables, this, recyclerView) - recyclerView.layoutManager = UIHelper.setMainLinearManagers(requireContext()) - recyclerView.adapter = nonStandardVariableAdapter + binding.editCard.nonStandardVariableRecyclerView.also { + nonStandardVariableAdapter = NonStandardVariableAdapter(variables, this@TransferFragment, it) + it.layoutManager = UIHelper.setMainLinearManagers(requireContext()) + it.adapter = nonStandardVariableAdapter + } } private fun updateNonStandardForSummaryCard(variables: LinkedHashMap) { - val recyclerView = binding.summaryCard.nonStandardSummaryRecycler - recyclerView.layoutManager = UIHelper.setMainLinearManagers(requireContext()) - nonStandardSummaryAdapter = NonStandardSummaryAdapter(variables) - recyclerView.adapter = nonStandardSummaryAdapter + binding.summaryCard.nonStandardSummaryRecycler.apply { + layoutManager = UIHelper.setMainLinearManagers(requireContext()) + adapter = NonStandardSummaryAdapter(variables) + } } private fun setRecipientHint(action: HoverAction) { @@ -361,6 +375,62 @@ class TransferFragment : AbstractFormFragment(), ActionSelect.HighlightListener, accountDropdown.setState(getString(R.string.channel_request_fieldinfo, data.institutionId.toString()), AbstractStatefulInput.INFO) } + private fun checkForBonus() { + if (args.transactionType == HoverAction.AIRTIME) { + val bonuses = bonusViewModel.bonuses.value + if (!bonuses.isNullOrEmpty()) + showBonusBanner(bonuses) + } + } + + private fun showBonusBanner(bonuses: List) = with(binding.bonusLayout) { + val channelId = bonuses.first().userChannel + + cardBonus.visibility = View.VISIBLE + val bonus = bonuses.first() + val usingBonusChannel = channelsViewModel.activeChannel.value?.id == bonus.purchaseChannel + + if (usingBonusChannel) { + title.text = getString(R.string.congratulations) + message.text = getString(R.string.valid_account_bonus_msg) + cta.visibility = View.GONE + } else { + title.text = getString(R.string.get_extra_airtime) + message.text = getString(R.string.invalid_account_bonus_msg) + cta.apply { + visibility = View.VISIBLE + text = getString(R.string.top_up_with_mpesa) + setOnClickListener { + AnalyticsUtil.logAnalyticsEvent(getString(R.string.clicked_bonus_airtime_banner), requireActivity()) + channelsViewModel.setActiveChannelAndAccount(bonus.purchaseChannel, channelId) + } + } + } + } + + override fun showEdit(isEditing: Boolean) { + super.showEdit(isEditing) + + if (!isEditing) + binding.bonusLayout.cardBonus.visibility = View.GONE + else + checkForBonus() + } + + /** + * Handles instances where the active account is different from the bonus account to be used. + * ChannelId is fetched from the bonus object's user channel field. + * Channel and respective accounts are fetched before being passed to account dropdown + */ + private fun updateAccountDropdown() = lifecycleScope.launch(Dispatchers.IO) { + val bonus = bonusViewModel.getBonusByChannelId(args.channelId) + + bonus?.let { + val channel = channelsViewModel.getChannel(bonus.userChannel) + channelsViewModel.setActiveChannelAndAccount(bonus.purchaseChannel, channel!!.id) + } ?: run { Timber.e("Bonus cannot be found") } + } + override fun onDestroyView() { super.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 13f7eb95c..4ff899618 100644 --- a/app/src/main/java/com/hover/stax/transfers/TransferViewModel.kt +++ b/app/src/main/java/com/hover/stax/transfers/TransferViewModel.kt @@ -46,7 +46,7 @@ class TransferViewModel(application: Application, repo: DatabaseRepo) : Abstract action?.let { val contact = repo.getContactAsync(transaction.counterparty_id) - autoFill(transaction.amount.toString(), contact, AutofillData(action.to_institution_id, transaction.channel_id, transaction.accountId, true)) + autoFill(transaction.amount.toInt().toString(), contact, AutofillData(action.to_institution_id, transaction.channel_id, transaction.accountId, true)) } } } @@ -58,7 +58,6 @@ class TransferViewModel(application: Application, repo: DatabaseRepo) : Abstract } fun setContact(sc: StaxContact?) = sc?.let { - Timber.i("contact is not null when posted") contact.postValue(it) } @@ -74,10 +73,10 @@ class TransferViewModel(application: Application, repo: DatabaseRepo) : Abstract } fun setRecipientSmartly(contactNum: String?, channel: Channel) { - contactNum.let { + contactNum?.let { viewModelScope.launch(Dispatchers.IO) { try { - val formattedPhone = PhoneHelper.getInternationalNumber(channel.countryAlpha2, contactNum) + val formattedPhone = PhoneHelper.getInternationalNumber(channel.countryAlpha2, it) val sc = repo.getContactByPhone(formattedPhone) sc?.let { contact.postValue(it) } } catch (e: NumberFormatException) { 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 0e7f65f3d..e303c6f3e 100644 --- a/app/src/main/java/com/hover/stax/utils/UIHelper.kt +++ b/app/src/main/java/com/hover/stax/utils/UIHelper.kt @@ -9,6 +9,7 @@ import android.os.Build import android.text.SpannableString import android.text.style.UnderlineSpan import android.view.View +import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView import android.widget.Toast @@ -76,17 +77,25 @@ object UIHelper { } } - fun loadImage(fragment: Fragment, url: String, imageView: ImageView) = GlideApp.with(fragment) + fun ImageView.loadImage(fragment: Fragment, url: String) = GlideApp.with(fragment) .load(url) .placeholder(R.drawable.icon_bg_circle) .circleCrop() - .into(imageView) + .override(100) + .into(this) - fun loadImage(context: Context, url: String, imageView: ImageView) = GlideApp.with(context) + fun ImageView.loadImage(context: Context, url: String) = GlideApp.with(context) .load(url) .placeholder(R.drawable.icon_bg_circle) .circleCrop() - .into(imageView) + .override(100) + .into(this) + + fun ImageButton.loadImage(context: Context, url: String) = GlideApp.with(context) + .load(url) + .placeholder(R.drawable.icon_bg_circle) + .circleCrop() + .into(this) fun loadImage(context: Context, url: String, target: CustomTarget) = GlideApp.with(context) .load(url) 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 93c5cf8d3..e0770656f 100644 --- a/app/src/main/java/com/hover/stax/views/StaxCardView.kt +++ b/app/src/main/java/com/hover/stax/views/StaxCardView.kt @@ -12,6 +12,7 @@ import androidx.core.content.ContextCompat import com.hover.stax.R import com.hover.stax.databinding.StaxCardViewBinding import com.hover.stax.utils.UIHelper +import com.hover.stax.utils.UIHelper.loadImage open class StaxCardView(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) { @@ -86,7 +87,7 @@ open class StaxCardView(context: Context, attrs: AttributeSet) : FrameLayout(con } fun setIcon(iconUrl: String) { - UIHelper.loadImage(context, iconUrl, binding.backButton) + binding.backButton.loadImage(context, iconUrl) } fun setOnClickIcon(listener: OnClickListener?) { diff --git a/app/src/main/res/drawable/ic_bonus.xml b/app/src/main/res/drawable/ic_bonus.xml new file mode 100644 index 000000000..25c8dae51 --- /dev/null +++ b/app/src/main/res/drawable/ic_bonus.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_info_red.xml b/app/src/main/res/drawable/ic_info_red.xml deleted file mode 100644 index b087412db..000000000 --- a/app/src/main/res/drawable/ic_info_red.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml index c36f0c175..0f5d81dde 100644 --- a/app/src/main/res/drawable/ic_warning.xml +++ b/app/src/main/res/drawable/ic_warning.xml @@ -1,5 +1,22 @@ - - + + + + + + + + diff --git a/app/src/main/res/drawable/stax_slide_1.jpg b/app/src/main/res/drawable/stax_slide_1.jpg deleted file mode 100644 index c87f1f008..000000000 Binary files a/app/src/main/res/drawable/stax_slide_1.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/stax_slide_3.jpg b/app/src/main/res/drawable/stax_slide_3.jpg deleted file mode 100644 index f2f85ec40..000000000 Binary files a/app/src/main/res/drawable/stax_slide_3.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/text_btn_selector.xml b/app/src/main/res/drawable/text_btn_selector.xml new file mode 100644 index 000000000..8e805ac85 --- /dev/null +++ b/app/src/main/res/drawable/text_btn_selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/card_bonuses.xml b/app/src/main/res/layout/card_bonuses.xml new file mode 100644 index 000000000..e3b231972 --- /dev/null +++ b/app/src/main/res/layout/card_bonuses.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 4aa0a4c0c..dbe66ebee 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -13,8 +13,8 @@ android:id="@+id/header" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="horizontal" android:layout_marginTop="@dimen/margin_5" + android:orientation="horizontal" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -39,12 +39,23 @@ app:layout_goneMarginTop="@dimen/margin_5" /> + + + app:layout_constraintTop_toBottomOf="@id/bonusCard" /> + +