├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── mnsons │ │ └── offlinebank │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── actions.json │ ├── java │ │ └── com │ │ │ └── mnsons │ │ │ └── offlinebank │ │ │ ├── ApplicationClass.kt │ │ │ ├── contracts │ │ │ ├── BuyAirtimeContract.kt │ │ │ ├── CheckBankBalanceContract.kt │ │ │ ├── MoneyTransferContract.kt │ │ │ ├── USSDResult.kt │ │ │ ├── access │ │ │ │ └── FetchAccessBankOtherBanksContract.kt │ │ │ ├── base │ │ │ │ └── BaseStringOnlyInputContract.kt │ │ │ └── gtb │ │ │ │ └── FetchGTBankOtherBanksFirstPageContract.kt │ │ │ ├── data │ │ │ └── cache │ │ │ │ ├── impl │ │ │ │ ├── BanksCache.kt │ │ │ │ ├── CoreSharedPrefManager.kt │ │ │ │ ├── SettingsCache.kt │ │ │ │ └── TransactionsCache.kt │ │ │ │ └── room │ │ │ │ ├── Converters.kt │ │ │ │ ├── DBClass.kt │ │ │ │ ├── dao │ │ │ │ ├── BankMenuDao.kt │ │ │ │ ├── BanksDao.kt │ │ │ │ └── TransactionsDao.kt │ │ │ │ └── entities │ │ │ │ ├── BankCacheModel.kt │ │ │ │ ├── BankMenuCacheModel.kt │ │ │ │ └── TransactionCacheModel.kt │ │ │ ├── di │ │ │ └── modules │ │ │ │ ├── CacheModule.kt │ │ │ │ └── UtilsModule.kt │ │ │ ├── model │ │ │ ├── BankMenuModel.kt │ │ │ ├── Mappers.kt │ │ │ ├── MoneyTransferModel.kt │ │ │ ├── bank │ │ │ │ └── BankModel.kt │ │ │ ├── buyairtime │ │ │ │ └── BuyAirtimeModel.kt │ │ │ ├── transaction │ │ │ │ ├── SectionedTransactionModel.kt │ │ │ │ └── TransactionModel.kt │ │ │ └── user │ │ │ │ └── UserModel.kt │ │ │ ├── ui │ │ │ ├── commons │ │ │ │ ├── adapters │ │ │ │ │ ├── AccountBalanceAdapter.kt │ │ │ │ │ ├── BankMenuAdapter.kt │ │ │ │ │ ├── SelectionListener.kt │ │ │ │ │ ├── bank │ │ │ │ │ │ └── BankSelectionAdapter.kt │ │ │ │ │ └── transaction │ │ │ │ │ │ ├── SectionedTransactionsAdapter.kt │ │ │ │ │ │ └── TransactionsAdapter.kt │ │ │ │ ├── banks │ │ │ │ │ └── BanksPopulator.kt │ │ │ │ ├── base │ │ │ │ │ └── BaseRoundedBottomSheetDialogFragment.kt │ │ │ │ └── dialogs │ │ │ │ │ ├── SelectBottomSheet.kt │ │ │ │ │ ├── SelectFromMenuBottomSheet.kt │ │ │ │ │ └── TransactionStatusDialog.kt │ │ │ ├── main │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── accountbalance │ │ │ │ │ ├── AccountBalanceFragment.kt │ │ │ │ │ ├── AccountBalanceState.kt │ │ │ │ │ └── AccountBalanceViewModel.kt │ │ │ │ ├── buyairtime │ │ │ │ │ ├── BuyAirtimeFragment.kt │ │ │ │ │ ├── BuyAirtimeState.kt │ │ │ │ │ └── BuyAirtimeViewModel.kt │ │ │ │ ├── dashboard │ │ │ │ │ ├── DashboardFragment.kt │ │ │ │ │ ├── DashboardState.kt │ │ │ │ │ └── DashboardViewModel.kt │ │ │ │ ├── home │ │ │ │ │ ├── HomeFragment.kt │ │ │ │ │ ├── HomeViewModel.kt │ │ │ │ │ └── menu │ │ │ │ │ │ ├── MenuAction.kt │ │ │ │ │ │ ├── MenuAdapter.kt │ │ │ │ │ │ └── MenuItemClickListener.kt │ │ │ │ ├── presentation │ │ │ │ │ ├── MainState.kt │ │ │ │ │ └── MainViewModel.kt │ │ │ │ ├── profile │ │ │ │ │ ├── addbank │ │ │ │ │ │ ├── AddBankFragment.kt │ │ │ │ │ │ ├── AddBankState.kt │ │ │ │ │ │ └── AddBankViewModel.kt │ │ │ │ │ └── userdetails │ │ │ │ │ │ └── UserDetailsFragment.kt │ │ │ │ └── transfermoney │ │ │ │ │ ├── TransferMoneyFragment.kt │ │ │ │ │ ├── TransferMoneyState.kt │ │ │ │ │ └── TransferMoneyViewModel.kt │ │ │ └── onboarding │ │ │ │ ├── OnBoardingActivity.kt │ │ │ │ ├── collectuserdetails │ │ │ │ └── CollectUserDetailsFragment.kt │ │ │ │ ├── done │ │ │ │ └── AllDoneFragment.kt │ │ │ │ ├── presentation │ │ │ │ ├── OnBoardingState.kt │ │ │ │ └── OnBoardingViewModel.kt │ │ │ │ ├── selectuserbanks │ │ │ │ ├── BankTransferMenuIndexer.kt │ │ │ │ └── SelectUserBanksFragment.kt │ │ │ │ └── setingup │ │ │ │ └── SettingUpFragment.kt │ │ │ └── utils │ │ │ ├── BuyAirtimeUtil.kt │ │ │ ├── CheckBalanceUtil.kt │ │ │ ├── Constants.kt │ │ │ ├── TransactionUtil.kt │ │ │ ├── TransferMoneyUtil.kt │ │ │ ├── ext │ │ │ ├── AutoClearedValue.kt │ │ │ ├── ContextExtensions.kt │ │ │ ├── FragmentExtensions.kt │ │ │ ├── LifecycleOwnerExtensions.kt │ │ │ ├── ListExt.kt │ │ │ ├── LiveDataExtension.kt │ │ │ ├── RecyclerViewExtensions.kt │ │ │ ├── StringExt.kt │ │ │ └── ViewExtensions.kt │ │ │ ├── flow │ │ │ ├── PostExecutionThread.kt │ │ │ └── PostExecutionThreadImpl.kt │ │ │ └── livedata │ │ │ └── NonNullObserver.kt │ └── res │ │ ├── anim │ │ ├── slide_in_up.xml │ │ └── slide_out_down.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── activity.xml │ │ ├── bottom_sheet_background_drawable.xml │ │ ├── home.xml │ │ ├── ic_access.xml │ │ ├── ic_airtime.xml │ │ ├── ic_airtime_purchase_icon.xml │ │ ├── ic_airtime_purchase_summary.xml │ │ ├── ic_bank_logo.xml │ │ ├── ic_bank_not_selected.xml │ │ ├── ic_bank_selected.xml │ │ ├── ic_bank_transfer_icon.xml │ │ ├── ic_bank_transfer_summary.xml │ │ ├── ic_bankable_.xml │ │ ├── ic_baseline_arrow_back_24.xml │ │ ├── ic_bills.xml │ │ ├── ic_dashboard_black_24dp.xml │ │ ├── ic_dummy_bank_logo.png │ │ ├── ic_dummy_user_avatar.png │ │ ├── ic_ellipses.xml │ │ ├── ic_filter.xml │ │ ├── ic_gtbank.xml │ │ ├── ic_home_black_24dp.xml │ │ ├── ic_home_indicator.xml │ │ ├── ic_internet.xml │ │ ├── ic_item_bank_background.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_notifications_black_24dp.xml │ │ ├── ic_official_building.xml │ │ ├── ic_profile_circle.xml │ │ ├── ic_search.xml │ │ ├── ic_transaction_failure.xml │ │ ├── ic_transaction_success.xml │ │ ├── ic_uba.xml │ │ ├── ic_zenith.xml │ │ ├── profile.xml │ │ └── transfer.xml │ │ ├── font │ │ ├── maisonneue_bold.ttf │ │ ├── maisonneue_bold_italic.ttf │ │ ├── maisonneue_book.ttf │ │ ├── maisonneue_book_italic.ttf │ │ ├── maisonneue_demi.ttf │ │ ├── maisonneue_demi_italic.ttf │ │ ├── maisonneue_light.ttf │ │ ├── maisonneue_light_italic.ttf │ │ ├── maisonneue_medium.ttf │ │ └── maisonneue_medium_italic.ttf │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_on_boarding.xml │ │ ├── fragment_account_balance.xml │ │ ├── fragment_add_bank.xml │ │ ├── fragment_all_done.xml │ │ ├── fragment_buy_airtime.xml │ │ ├── fragment_collect_user_details.xml │ │ ├── fragment_dashboard.xml │ │ ├── fragment_home.xml │ │ ├── fragment_notifications.xml │ │ ├── fragment_select_user_banks.xml │ │ ├── fragment_setting_up.xml │ │ ├── fragment_transfer_money.xml │ │ ├── fragment_user_details.xml │ │ ├── item_account_balance.xml │ │ ├── item_bank.xml │ │ ├── item_sectioned_transaction.xml │ │ ├── item_transaction.xml │ │ ├── layout_buy_airtime_details.xml │ │ ├── layout_enter_ussd_pin.xml │ │ ├── layout_save_as_beneficiary.xml │ │ ├── layout_select_bank.xml │ │ ├── layout_select_bank_bottom_sheet.xml │ │ ├── layout_successful_transaction.xml │ │ ├── layout_transaction_outcome.xml │ │ ├── layout_transfer_money_details.xml │ │ └── menu_item_card.xml │ │ ├── menu │ │ └── bottom_nav_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ ├── mobile_navigation.xml │ │ └── onboarding_navigation.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values-v23 │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── mnsons │ └── offlinebank │ └── ExampleUnitTest.kt ├── app_icon.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── readme.md └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | OfflineBank -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/mnsons/offlinebank/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.mnsons.offlinebank", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 17 | 18 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/assets/actions.json: -------------------------------------------------------------------------------- 1 | { 2 | "Transfer Money": [ 3 | { 4 | "bankName": "GT Bank", 5 | "bankRootCode": "*737#", 6 | "hoverActionId": "", 7 | "setup": [ 8 | "Dial RootCode", 9 | "Select Option 5" 10 | ], 11 | "steps": [ 12 | "Enter Amount", 13 | "Enter Account Number", 14 | "Display Menu Select Recipient Bank", 15 | "Enter Pin", 16 | "Display Menu Save Beneficiary" 17 | ] 18 | }, 19 | { 20 | "bankName": "Union Bank", 21 | "bankRootCode": "*826#", 22 | "hoverActionId": "", 23 | "setup": [ 24 | "Dial RootCode", 25 | "Select Option 5" 26 | ], 27 | "steps": [ 28 | "Enter Amount", 29 | "Enter Account Number", 30 | "Display Menu Select Recipient Bank", 31 | "Enter Pin", 32 | "Display Menu Save Beneficiary" 33 | ] 34 | } 35 | ], 36 | "Buy Airtime": [ 37 | { 38 | "bankName": "GT Bank", 39 | "bankRootCode": "*737#", 40 | "hoverActionId": "", 41 | "setup": [ 42 | "Dial RootCode", 43 | "Select Option 2" 44 | ], 45 | "steps": [ 46 | "Enter Phone Number", 47 | "Enter Amount", 48 | "Enter Pin", 49 | "Display Menu Save Beneficiary" 50 | ] 51 | }, 52 | { 53 | "bankName": "Union Bank", 54 | "bankRootCode": "*826#", 55 | "hoverActionId": "", 56 | "setup": [ 57 | "Dial RootCode", 58 | "Select Option 2" 59 | ], 60 | "steps": [ 61 | "Enter Phone Number", 62 | "Enter Amount", 63 | "Enter Pin", 64 | "Display Menu Save Beneficiary" 65 | ] 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ApplicationClass.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank 17 | 18 | import android.app.Application 19 | import dagger.hilt.android.HiltAndroidApp 20 | 21 | 22 | @HiltAndroidApp 23 | class ApplicationClass : Application() { 24 | 25 | override fun onCreate() { 26 | super.onCreate() 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/contracts/BuyAirtimeContract.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.contracts 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | import androidx.activity.result.contract.ActivityResultContract 8 | import com.hover.sdk.api.HoverParameters 9 | import com.mnsons.offlinebank.R 10 | import com.mnsons.offlinebank.model.buyairtime.BuyAirtimeModel 11 | import com.mnsons.offlinebank.utils.Constants 12 | import java.security.InvalidParameterException 13 | 14 | class BuyAirtimeContract : ActivityResultContract>() { 15 | 16 | private lateinit var context: Context 17 | 18 | override fun createIntent(context: Context, input: BuyAirtimeModel?): Intent { 19 | this.context = context 20 | 21 | input?.let { 22 | return HoverParameters.Builder(context) 23 | .request(it.actionId) 24 | .extra(Constants.EXTRA_AMOUNT, it.amount) 25 | .extra(Constants.EXTRA_PHONE_NUMBER, it.phoneNumber) 26 | .style(R.style.Theme_Hover) 27 | .buildIntent() 28 | } ?: throw InvalidParameterException("Please enter the correct parameters") 29 | } 30 | 31 | override fun parseResult(resultCode: Int, intent: Intent?): USSDResult { 32 | if (resultCode != Activity.RESULT_OK) return USSDResult(context.getString(R.string.error_index_)) 33 | if (intent == null) return USSDResult(context.getString(R.string.error_index_)) 34 | 35 | val sessionTextArr: Array = 36 | intent.getStringArrayExtra("session_messages") ?: emptyArray() 37 | sessionTextArr.forEach { 38 | Log.d(javaClass.simpleName, it) 39 | } 40 | Log.d(javaClass.simpleName, intent.toString()) 41 | 42 | return if (sessionTextArr.isNotEmpty()) { 43 | return parseSessionMessage(sessionTextArr.last()) 44 | } else { 45 | USSDResult(context.getString(R.string.error_index_)) 46 | } 47 | } 48 | 49 | private fun parseSessionMessage(text: String): USSDResult { 50 | return when { 51 | text.contains("processing", ignoreCase = true) or text.contains( 52 | "successful", 53 | ignoreCase = true 54 | ) -> { 55 | USSDResult(context.getString(R.string.transaction_successful), Unit) 56 | } 57 | text.contains("No Account is funded") -> { 58 | USSDResult(context.getString(R.string.transaction_n0t_successful)) 59 | } 60 | else -> { 61 | USSDResult(context.getString(R.string.error_index_)) 62 | } 63 | } 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/contracts/CheckBankBalanceContract.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.contracts 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | import androidx.activity.result.contract.ActivityResultContract 8 | import com.hover.sdk.api.HoverParameters 9 | import com.mnsons.offlinebank.R 10 | import java.security.InvalidParameterException 11 | 12 | class CheckBankBalanceContract : ActivityResultContract>() { 13 | 14 | private lateinit var context: Context 15 | 16 | override fun createIntent(context: Context, input: String?): Intent { 17 | this.context = context 18 | input?.let { 19 | return HoverParameters.Builder(context) 20 | .request(it) 21 | .style(R.style.Theme_Hover) 22 | .buildIntent() 23 | } ?: throw InvalidParameterException("Please enter the correct parameters") 24 | } 25 | 26 | override fun parseResult(resultCode: Int, intent: Intent?): USSDResult { 27 | if (resultCode != Activity.RESULT_OK) return USSDResult(context.getString(R.string.error_index_)) 28 | if (intent == null) return USSDResult(context.getString(R.string.error_index_)) 29 | 30 | val sessionTextArr: Array = 31 | intent.getStringArrayExtra("session_messages") ?: emptyArray() 32 | sessionTextArr.forEach { 33 | Log.d(CheckBankBalanceContract::class.java.simpleName, it) 34 | } 35 | Log.d(CheckBankBalanceContract::class.java.simpleName, intent.toString()) 36 | 37 | return if (sessionTextArr.isNotEmpty()) { 38 | parseSessionMessage(sessionTextArr.last()) 39 | } else { 40 | USSDResult(context.getString(R.string.error_index_)) 41 | } 42 | } 43 | 44 | private fun parseSessionMessage(text: String): USSDResult { 45 | return when { 46 | text.contains("NAME") or text.contains("BVN") -> { 47 | USSDResult(context.getString(R.string.transaction_successful), text) 48 | } 49 | text.contains("No Account is funded") -> { 50 | USSDResult(context.getString(R.string.transaction_n0t_successful)) 51 | } 52 | else -> { 53 | USSDResult(context.getString(R.string.error_index_)) 54 | } 55 | } 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/contracts/MoneyTransferContract.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.contracts 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | import androidx.activity.result.contract.ActivityResultContract 8 | import com.hover.sdk.api.HoverParameters 9 | import com.mnsons.offlinebank.R 10 | import com.mnsons.offlinebank.model.MoneyTransferModel 11 | import java.security.InvalidParameterException 12 | 13 | 14 | class MoneyTransferContract : ActivityResultContract>() { 15 | 16 | private lateinit var context: Context 17 | 18 | override fun createIntent(context: Context, input: MoneyTransferModel?): Intent { 19 | this.context = context 20 | input?.let { 21 | return HoverParameters.Builder(context) 22 | .request(it.actionId) 23 | .extra("amount", it.amount) 24 | .extra("bankAccountNumber", it.accountNumber) 25 | .extra("recipientBank", it.recipientBank.toString()) 26 | .style(R.style.Theme_Hover) 27 | .buildIntent() 28 | } ?: throw InvalidParameterException() 29 | } 30 | 31 | override fun parseResult(resultCode: Int, intent: Intent?): USSDResult { 32 | if (resultCode != Activity.RESULT_OK) return USSDResult(context.getString(R.string.transaction_n0t_successful)) 33 | if (intent == null) return USSDResult(context.getString(R.string.transaction_n0t_successful)) 34 | 35 | val sessionTextArr: Array = 36 | intent.getStringArrayExtra("session_messages") ?: emptyArray() 37 | sessionTextArr.forEach { 38 | Log.d(MoneyTransferContract::class.java.simpleName, it) 39 | } 40 | Log.d(MoneyTransferContract::class.java.simpleName, intent.toString()) 41 | 42 | return if (sessionTextArr.isNotEmpty()) { 43 | return parseSessionMessage(sessionTextArr.last()) 44 | } else { 45 | USSDResult(context.getString(R.string.transaction_n0t_successful)) 46 | } 47 | } 48 | 49 | 50 | private fun parseSessionMessage(text: String): USSDResult { 51 | return when { 52 | text.contains("successful") or text.contains("beneficiary") or text.contains("success") -> { 53 | USSDResult(context.getString(R.string.transaction_successful), Unit) 54 | } 55 | text.contains("No Account is funded") -> { 56 | USSDResult(context.getString(R.string.transaction_n0t_successful)) 57 | } 58 | else -> { 59 | USSDResult(context.getString(R.string.error_index_)) 60 | } 61 | } 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/contracts/USSDResult.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.contracts 2 | 3 | data class USSDResult( 4 | val message: String? = null, 5 | var data: T? = null 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/contracts/access/FetchAccessBankOtherBanksContract.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.contracts.access 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import androidx.activity.result.contract.ActivityResultContract 7 | import com.hover.sdk.api.HoverParameters 8 | import com.mnsons.offlinebank.R 9 | import com.mnsons.offlinebank.contracts.USSDResult 10 | import com.mnsons.offlinebank.model.BankMenuModel 11 | 12 | class FetchAccessBankOtherBanksContract : 13 | ActivityResultContract>>() { 14 | 15 | private lateinit var context: Context 16 | 17 | override fun createIntent(context: Context, input: Unit?): Intent { 18 | this.context = context 19 | return HoverParameters.Builder(context) 20 | .request("51dfe430") 21 | .style(R.style.Theme_Hover) 22 | .buildIntent() 23 | } 24 | 25 | override fun parseResult(resultCode: Int, intent: Intent?): USSDResult> { 26 | if (resultCode != Activity.RESULT_OK) return USSDResult(context.getString(R.string.error_fetching_)) 27 | if (intent == null) return USSDResult(context.getString(R.string.error_fetching_)) 28 | 29 | val sessionTextArr: Array = 30 | intent.getStringArrayExtra("session_messages") ?: emptyArray() 31 | return if (sessionTextArr.isNotEmpty()) { 32 | return parseResult(sessionTextArr.last()) 33 | } else { 34 | USSDResult(context.getString(R.string.error_fetching_bank_acc_balance)) 35 | } 36 | } 37 | 38 | private fun parseResult(result: String): USSDResult> { 39 | val list = mutableListOf() 40 | result.lines().forEach { 41 | //Could be improved with a regex to add only strings that match gtbanks menu style 42 | if ((it.contains("more", true) or it.contains("Please", true)).not()) { 43 | val data = it.split(">") 44 | if(data.size > 1){ 45 | list.add(BankMenuModel(data[1], data[0].toInt())) 46 | } 47 | } 48 | } 49 | return USSDResult(data = list) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/contracts/base/BaseStringOnlyInputContract.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.contracts.base 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.activity.result.contract.ActivityResultContract 6 | import com.hover.sdk.api.HoverParameters 7 | import java.security.InvalidParameterException 8 | 9 | abstract class BaseStringOnlyInputContract : ActivityResultContract() { 10 | 11 | override fun createIntent(context: Context, input: String?): Intent { 12 | input?.let { 13 | return HoverParameters.Builder(context) 14 | .request(it) 15 | .buildIntent() 16 | } ?: throw InvalidParameterException("Please enter the correct parameters") 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/contracts/gtb/FetchGTBankOtherBanksFirstPageContract.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.contracts.gtb 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import androidx.activity.result.contract.ActivityResultContract 7 | import com.hover.sdk.api.HoverParameters 8 | import com.mnsons.offlinebank.R 9 | import com.mnsons.offlinebank.contracts.USSDResult 10 | import com.mnsons.offlinebank.model.BankMenuModel 11 | 12 | class FetchGTBankOtherBanksFirstPageContract : 13 | ActivityResultContract>>() { 14 | 15 | private lateinit var context: Context 16 | 17 | override fun createIntent(context: Context, input: Unit?): Intent { 18 | this.context = context 19 | return HoverParameters.Builder(context) 20 | .request("896119e1") 21 | .style(R.style.Theme_Hover) 22 | .buildIntent() 23 | } 24 | 25 | override fun parseResult(resultCode: Int, intent: Intent?): USSDResult> { 26 | if (resultCode != Activity.RESULT_OK) return USSDResult(context.getString(R.string.error_fetching_bank_acc_balance)) 27 | if (intent == null) return USSDResult(context.getString(R.string.error_fetching_bank_acc_balance)) 28 | 29 | val sessionTextArr: Array = 30 | intent.getStringArrayExtra("session_messages") ?: emptyArray() 31 | return if (sessionTextArr.isNotEmpty()) { 32 | return parseResult(sessionTextArr.last()) 33 | } else { 34 | USSDResult(context.getString(R.string.error_fetching_bank_acc_balance)) 35 | } 36 | } 37 | 38 | private fun parseResult(result: String): USSDResult> { 39 | val list = mutableListOf() 40 | result.lines().forEach { 41 | //Could be improved with a regex to add only strings that match gtbanks menu style 42 | if ((it.contains("more", true) or it.contains("Please", true)).not()) { 43 | val data = it.split(".") 44 | if(data.size > 1){ 45 | list.add(BankMenuModel(data[1], data[0].toInt())) 46 | } 47 | } 48 | } 49 | return USSDResult(data = list) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/data/cache/impl/BanksCache.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.data.cache.impl 2 | 3 | import com.mnsons.offlinebank.data.cache.room.dao.BankMenuDao 4 | import com.mnsons.offlinebank.data.cache.room.dao.BanksDao 5 | import com.mnsons.offlinebank.data.cache.room.entities.BankCacheModel 6 | import com.mnsons.offlinebank.data.cache.room.entities.BankMenuCacheModel 7 | import com.mnsons.offlinebank.model.BankMenuModel 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.flow 10 | import javax.inject.Inject 11 | 12 | class BanksCache @Inject constructor( 13 | private val banksDao: BanksDao, 14 | private val bankMenuDao: BankMenuDao 15 | ) { 16 | 17 | suspend fun saveBanks(list: List) { 18 | banksDao.insert(list) 19 | } 20 | 21 | suspend fun saveBank(bank: BankCacheModel) { 22 | banksDao.insert(bank) 23 | } 24 | 25 | suspend fun saveBankMenuData(bankId: Int, list: List) { 26 | var bankMenu = bankMenuDao.getBankMenu(bankId) 27 | if (bankMenu != null) { 28 | val menuItems = bankMenu.menuItems.toMutableList() 29 | menuItems.addAll(list) 30 | bankMenuDao.insert(bankMenu.copy(menuItems = menuItems)) 31 | } else { 32 | bankMenu = BankMenuCacheModel(bankId, list) 33 | bankMenuDao.insert(bankMenu) 34 | } 35 | } 36 | 37 | fun getBankMenu(id: Int): Flow> { 38 | return flow { 39 | //emit(bankMenuDao.getBankMenu(id)?.menuItems ?: emptyList()) 40 | } 41 | } 42 | 43 | fun getBanks(): Flow> { 44 | return banksDao.getAllBanks() 45 | } 46 | 47 | suspend fun isCacheEmpty(): Boolean { 48 | return banksDao.getAllBanksCount() > 0 49 | } 50 | 51 | suspend fun clearBanks() { 52 | return banksDao.deleteAllBanks() 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/data/cache/impl/CoreSharedPrefManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.data.cache.impl 17 | 18 | import android.content.Context 19 | import android.content.SharedPreferences 20 | 21 | open class CoreSharedPrefManager(context: Context) { 22 | 23 | private val sharedPreferences: SharedPreferences = context.getSharedPreferences( 24 | APP_NAME, 25 | Context.MODE_PRIVATE 26 | ) 27 | 28 | private val sharedPreferencesEditor: SharedPreferences.Editor = sharedPreferences.edit() 29 | 30 | private fun delete(key: String) { 31 | if (sharedPreferences.contains(key)) { 32 | sharedPreferencesEditor.remove(key).commit() 33 | } 34 | } 35 | 36 | protected fun savePref(key: String, value: Any?) { 37 | delete(key) 38 | 39 | when { 40 | value is Boolean -> sharedPreferencesEditor.putBoolean(key, (value as Boolean?)!!) 41 | value is Int -> sharedPreferencesEditor.putInt(key, (value as Int?)!!) 42 | value is Float -> sharedPreferencesEditor.putFloat(key, (value as Float?)!!) 43 | value is Long -> sharedPreferencesEditor.putLong(key, (value as Long?)!!) 44 | value is String -> sharedPreferencesEditor.putString(key, value as String?) 45 | value is Enum<*> -> sharedPreferencesEditor.putString(key, value.toString()) 46 | value != null -> throw RuntimeException("Attempting to save non-primitive preference") 47 | } 48 | 49 | sharedPreferencesEditor.commit() 50 | } 51 | 52 | protected fun getPref(key: String): T { 53 | return sharedPreferences.all[key] as T 54 | } 55 | 56 | protected fun getPref(key: String, defValue: T?): T? { 57 | val returnValue = sharedPreferences.all[key] as T 58 | return returnValue ?: defValue 59 | } 60 | 61 | fun clearAll() { 62 | sharedPreferencesEditor.clear() 63 | } 64 | 65 | 66 | companion object { 67 | const val APP_NAME = "BANKABLE" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/data/cache/impl/SettingsCache.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.data.cache.impl 2 | 3 | import android.content.Context 4 | import dagger.hilt.android.qualifiers.ApplicationContext 5 | import javax.inject.Inject 6 | 7 | class SettingsCache @Inject constructor(@ApplicationContext val context: Context) : CoreSharedPrefManager(context) { 8 | 9 | fun fetchUserFirstName(): String? { 10 | return getPref(USER_FIRST_NAME) 11 | } 12 | 13 | fun fetchUserLastName(): String? { 14 | return getPref(USER_LAST_NAME) 15 | } 16 | 17 | fun fetchUserPhone(): String? { 18 | return getPref(USER_PHONE) 19 | } 20 | 21 | fun setUserPhone(phone: String) { 22 | savePref(USER_PHONE, phone) 23 | } 24 | 25 | fun setUserLastName(userName: String) { 26 | savePref(USER_LAST_NAME, userName) 27 | } 28 | 29 | fun setUserFirstName(userName: String) { 30 | savePref(USER_FIRST_NAME, userName) 31 | } 32 | 33 | fun userDataExists():Boolean{ 34 | return fetchUserFirstName() != null && fetchUserLastName()!=null && fetchUserPhone()!=null 35 | } 36 | 37 | companion object { 38 | private const val USER_FIRST_NAME = "USER_FIRST_NAME" 39 | private const val USER_LAST_NAME = "USER_LAST_NAME" 40 | private const val USER_PHONE = "USER_PHONE" 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/data/cache/impl/TransactionsCache.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.data.cache.impl 2 | 3 | import com.mnsons.offlinebank.data.cache.room.dao.TransactionsDao 4 | import com.mnsons.offlinebank.model.mapInto 5 | import com.mnsons.offlinebank.model.toTransactionCacheModel 6 | import com.mnsons.offlinebank.model.toTransactionModel 7 | import com.mnsons.offlinebank.model.transaction.TransactionModel 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.map 10 | import javax.inject.Inject 11 | 12 | class TransactionsCache @Inject constructor( 13 | private val transactionsDao: TransactionsDao 14 | ) { 15 | 16 | suspend fun saveTransactions(list: List) { 17 | transactionsDao.insert(list.mapInto { 18 | it.toTransactionCacheModel() 19 | }) 20 | } 21 | 22 | suspend fun saveTransaction(transaction: TransactionModel) { 23 | transactionsDao.insert(transaction.toTransactionCacheModel()) 24 | } 25 | 26 | fun getTransaction(): Flow> { 27 | return transactionsDao.getAllTransactions().map { 28 | it.mapInto { 29 | it.toTransactionModel() 30 | } 31 | } 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/data/cache/room/Converters.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.data.cache.room 17 | 18 | import androidx.room.TypeConverter 19 | import com.google.gson.Gson 20 | import com.google.gson.reflect.TypeToken 21 | import com.mnsons.offlinebank.model.BankMenuModel 22 | 23 | 24 | class Converters { 25 | 26 | @TypeConverter 27 | fun fromBankMenuModelString(value: String?): List? { 28 | val listType = object : TypeToken?>() { 29 | }.type 30 | return Gson().fromJson(value, listType) 31 | } 32 | 33 | @TypeConverter 34 | fun fromBankMenuModelList(data: List?): String { 35 | val gson = Gson() 36 | return gson.toJson(data) 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/data/cache/room/DBClass.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.data.cache.room 17 | 18 | 19 | import androidx.room.Database 20 | import androidx.room.RoomDatabase 21 | import androidx.room.TypeConverters 22 | import com.mnsons.offlinebank.data.cache.room.dao.BankMenuDao 23 | import com.mnsons.offlinebank.data.cache.room.dao.BanksDao 24 | import com.mnsons.offlinebank.data.cache.room.dao.TransactionsDao 25 | import com.mnsons.offlinebank.data.cache.room.entities.BankCacheModel 26 | import com.mnsons.offlinebank.data.cache.room.entities.BankMenuCacheModel 27 | import com.mnsons.offlinebank.data.cache.room.entities.TransactionCacheModel 28 | 29 | 30 | @Database( 31 | entities = [BankCacheModel::class, BankMenuCacheModel::class, TransactionCacheModel::class], 32 | version = 1, exportSchema = false 33 | ) 34 | @TypeConverters(Converters::class) 35 | abstract class DBClass : RoomDatabase() { 36 | 37 | abstract fun banksDao(): BanksDao 38 | 39 | abstract fun bankMenuDao(): BankMenuDao 40 | 41 | abstract fun transactionsDao(): TransactionsDao 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/data/cache/room/dao/BankMenuDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.data.cache.room.dao 17 | 18 | import androidx.room.Dao 19 | import androidx.room.Insert 20 | import androidx.room.OnConflictStrategy 21 | import androidx.room.Query 22 | import com.mnsons.offlinebank.data.cache.room.entities.BankMenuCacheModel 23 | 24 | @Dao 25 | interface BankMenuDao { 26 | 27 | @Insert(onConflict = OnConflictStrategy.REPLACE) 28 | suspend fun insert(menus: List) 29 | 30 | @Insert(onConflict = OnConflictStrategy.REPLACE) 31 | suspend fun insert(menu: BankMenuCacheModel) 32 | 33 | @Query("SELECT * FROM BANKS_MENU where id=:id") 34 | suspend fun getBankMenu(id: Int): BankMenuCacheModel? 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/data/cache/room/dao/BanksDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.data.cache.room.dao 17 | 18 | import androidx.room.Dao 19 | import androidx.room.Insert 20 | import androidx.room.OnConflictStrategy 21 | import androidx.room.Query 22 | import com.mnsons.offlinebank.data.cache.room.entities.BankCacheModel 23 | import kotlinx.coroutines.flow.Flow 24 | 25 | @Dao 26 | interface BanksDao { 27 | 28 | @Insert(onConflict = OnConflictStrategy.REPLACE) 29 | suspend fun insert(banks: List) 30 | 31 | @Insert(onConflict = OnConflictStrategy.REPLACE) 32 | suspend fun insert(bank: BankCacheModel) 33 | 34 | @Query("SELECT * FROM BANKS") 35 | fun getAllBanks(): Flow> 36 | 37 | @Query("SELECT COUNT(*) FROM BANKS") 38 | suspend fun getAllBanksCount(): Int 39 | 40 | @Query("DELETE FROM BANKS WHERE id =:id") 41 | suspend fun deleteBank(id: Int) 42 | 43 | @Query("DELETE FROM BANKS") 44 | suspend fun deleteAllBanks() 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/data/cache/room/dao/TransactionsDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.data.cache.room.dao 17 | 18 | import androidx.room.Dao 19 | import androidx.room.Insert 20 | import androidx.room.OnConflictStrategy 21 | import androidx.room.Query 22 | import com.mnsons.offlinebank.data.cache.room.entities.BankCacheModel 23 | import com.mnsons.offlinebank.data.cache.room.entities.TransactionCacheModel 24 | import kotlinx.coroutines.flow.Flow 25 | 26 | @Dao 27 | interface TransactionsDao { 28 | 29 | @Insert(onConflict = OnConflictStrategy.REPLACE) 30 | suspend fun insert(transactions: List) 31 | 32 | @Insert(onConflict = OnConflictStrategy.REPLACE) 33 | suspend fun insert(transaction: TransactionCacheModel) 34 | 35 | @Query("SELECT * FROM TRANSACTIONS") 36 | fun getAllTransactions(): Flow> 37 | 38 | @Query("DELETE FROM TRANSACTIONS WHERE id =:id") 39 | suspend fun deleteTransactions(id: Int) 40 | 41 | @Query("DELETE FROM TRANSACTIONS") 42 | suspend fun deleteAllTransactions() 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/data/cache/room/entities/BankCacheModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.data.cache.room.entities 17 | 18 | import androidx.room.Entity 19 | import androidx.room.PrimaryKey 20 | 21 | @Entity(tableName = "BANKS") 22 | data class BankCacheModel( 23 | @PrimaryKey 24 | var id: Int, 25 | var name: Int, 26 | var lastKnownBalance: Long, 27 | var sortCode: String, 28 | var imageURL: Int 29 | ) { 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/data/cache/room/entities/BankMenuCacheModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.data.cache.room.entities 17 | 18 | import androidx.room.Entity 19 | import androidx.room.PrimaryKey 20 | import com.mnsons.offlinebank.model.BankMenuModel 21 | 22 | @Entity(tableName = "BANKS_MENU") 23 | data class BankMenuCacheModel( 24 | @PrimaryKey 25 | var id: Int, 26 | var menuItems: List 27 | ) 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/data/cache/room/entities/TransactionCacheModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.data.cache.room.entities 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.mnsons.offlinebank.model.transaction.TransactionStatus 6 | import com.mnsons.offlinebank.model.transaction.TransactionType 7 | 8 | @Entity(tableName = "TRANSACTIONS") 9 | data class TransactionCacheModel( 10 | @PrimaryKey 11 | var id: String, 12 | var amount: Double, 13 | var timestamp: Long, 14 | var type: String, 15 | var status: Int, 16 | var bank: String 17 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/di/modules/CacheModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.di.modules 17 | 18 | import android.content.Context 19 | import androidx.room.Room 20 | import com.mnsons.offlinebank.data.cache.room.DBClass 21 | import com.mnsons.offlinebank.data.cache.room.dao.BankMenuDao 22 | import com.mnsons.offlinebank.data.cache.room.dao.BanksDao 23 | import com.mnsons.offlinebank.data.cache.room.dao.TransactionsDao 24 | import dagger.Module 25 | import dagger.Provides 26 | import dagger.hilt.InstallIn 27 | import dagger.hilt.android.components.ApplicationComponent 28 | import dagger.hilt.android.qualifiers.ApplicationContext 29 | import javax.inject.Singleton 30 | 31 | @Module 32 | @InstallIn(ApplicationComponent::class) 33 | class CacheModule { 34 | 35 | 36 | @Singleton 37 | @Provides 38 | fun providesBanksDao(dBClass: DBClass): BanksDao { 39 | return dBClass.banksDao() 40 | } 41 | 42 | @Singleton 43 | @Provides 44 | fun providesTransactionsDao(dBClass: DBClass): TransactionsDao { 45 | return dBClass.transactionsDao() 46 | } 47 | 48 | 49 | @Singleton 50 | @Provides 51 | fun providesBankMenuDao(dBClass: DBClass): BankMenuDao { 52 | return dBClass.bankMenuDao() 53 | } 54 | 55 | @Singleton 56 | @Provides 57 | fun providesDB(@ApplicationContext context: Context): DBClass { 58 | return Room.databaseBuilder( 59 | context.applicationContext, 60 | DBClass::class.java, "mandsons_database" 61 | ).fallbackToDestructiveMigration().build() 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/di/modules/UtilsModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.di.modules 17 | 18 | import com.mnsons.offlinebank.utils.flow.PostExecutionThread 19 | import com.mnsons.offlinebank.utils.flow.PostExecutionThreadImpl 20 | import dagger.Binds 21 | import dagger.Module 22 | import dagger.hilt.InstallIn 23 | import dagger.hilt.android.components.ApplicationComponent 24 | 25 | @Module 26 | @InstallIn(ApplicationComponent::class) 27 | abstract class UtilsModule { 28 | 29 | @Binds 30 | abstract fun bindsPostExecutionThread(postExecutionThread: PostExecutionThreadImpl): PostExecutionThread 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/model/BankMenuModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.model 2 | 3 | data class BankMenuModel( 4 | val bankName: String, 5 | var id: Int 6 | ) 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/model/Mappers.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.model 2 | 3 | import com.mnsons.offlinebank.data.cache.room.entities.BankCacheModel 4 | import com.mnsons.offlinebank.data.cache.room.entities.TransactionCacheModel 5 | import com.mnsons.offlinebank.model.bank.BankModel 6 | import com.mnsons.offlinebank.model.transaction.TransactionModel 7 | import com.mnsons.offlinebank.model.transaction.TransactionStatus 8 | import com.mnsons.offlinebank.model.transaction.TransactionType 9 | import java.util.* 10 | 11 | fun List.mapInto(function: (input: I) -> O): List { 12 | return map { 13 | function.invoke(it) 14 | } 15 | } 16 | 17 | fun TransactionModel.toTransactionCacheModel(): TransactionCacheModel { 18 | return TransactionCacheModel( 19 | UUID.randomUUID().toString(), 20 | amount, 21 | timestamp, 22 | type.value, 23 | status.value, 24 | bank 25 | ) 26 | } 27 | 28 | fun TransactionCacheModel.toTransactionModel(): TransactionModel { 29 | return TransactionModel( 30 | amount, 31 | timestamp, 32 | TransactionType.fromString(type), 33 | TransactionStatus.fromString(status), 34 | bank 35 | ) 36 | } 37 | 38 | fun BankModel.toBankCacheModel(): BankCacheModel { 39 | return BankCacheModel( 40 | id, 41 | bankName, 42 | lastKnownBalance, 43 | sortCode, 44 | bankLogo 45 | ) 46 | } 47 | 48 | fun BankCacheModel.toBankModel(): BankModel { 49 | return BankModel( 50 | name, 51 | id, 52 | lastKnownBalance, 53 | sortCode, 54 | imageURL 55 | ) 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/model/MoneyTransferModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.model 2 | 3 | data class MoneyTransferModel( 4 | val recipientBank: Int? = null, 5 | val actionId: String? = null, 6 | val amount: String? = null, 7 | val accountNumber: String? = null, 8 | val bank: String? = null 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/model/bank/BankModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.model.bank 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.DrawableRes 5 | import kotlinx.android.parcel.Parcelize 6 | 7 | @Parcelize 8 | data class BankModel( 9 | val bankName: Int, 10 | var id: Int, 11 | var lastKnownBalance: Long, 12 | var sortCode: String, 13 | @DrawableRes 14 | val bankLogo: Int 15 | ) : Parcelable 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/model/buyairtime/BuyAirtimeModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.model.buyairtime 2 | 3 | data class BuyAirtimeModel( 4 | val actionId: String? = null, 5 | val amount: String? = null, 6 | val phoneNumber: String? = null, 7 | val bank:String? = null 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/model/transaction/SectionedTransactionModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.model.transaction 2 | 3 | data class SectionedTransactionModel( 4 | val day: String, 5 | val transactions: MutableList 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/model/transaction/TransactionModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.model.transaction 2 | 3 | data class TransactionModel( 4 | val amount: Double, 5 | val timestamp: Long, 6 | val type: TransactionType, 7 | val status: TransactionStatus, 8 | val bank: String 9 | ) 10 | 11 | enum class TransactionType(val value: String) { 12 | BANK_TRANSFER("Bank Transfer"), 13 | AIRTIME_PURCHASE("Airtime Purchase"), 14 | BALANCE_CHECK("Account Balance Check"); 15 | 16 | companion object { 17 | @JvmStatic 18 | fun fromString(category: String): TransactionType = 19 | values().find { value -> value.value == category } ?: AIRTIME_PURCHASE 20 | } 21 | } 22 | 23 | enum class TransactionStatus(val value: Int) { 24 | SUCCESS(1), 25 | PENDING(2), 26 | FAILED(3); 27 | 28 | companion object { 29 | @JvmStatic 30 | fun fromString(status: Int): TransactionStatus = 31 | values().find { value -> value.value == status } ?: PENDING 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/model/user/UserModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.model.user 2 | 3 | import com.mnsons.offlinebank.model.bank.BankModel 4 | 5 | data class UserModel( 6 | val firstName: String, 7 | val lastName: String, 8 | val phoneNumber: String, 9 | val banks: List 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/commons/adapters/AccountBalanceAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.commons.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.mnsons.offlinebank.R 8 | import kotlinx.android.synthetic.main.item_account_balance.view.* 9 | 10 | class AccountBalanceAdapter : 11 | RecyclerView.Adapter() { 12 | 13 | var all: List = mutableListOf() 14 | 15 | override fun onCreateViewHolder( 16 | parent: ViewGroup, 17 | viewType: Int 18 | ): AccountBalanceItemViewHolder { 19 | return AccountBalanceItemViewHolder( 20 | LayoutInflater.from(parent.context) 21 | .inflate(R.layout.item_account_balance, parent, false) 22 | ) 23 | } 24 | 25 | override fun getItemCount(): Int { 26 | return all.size 27 | } 28 | 29 | override fun onBindViewHolder(holderMenu: AccountBalanceItemViewHolder, position: Int) { 30 | holderMenu.bind(all[position]) 31 | } 32 | 33 | inner class AccountBalanceItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 34 | 35 | fun bind(balance: String) { 36 | itemView.tvBankNameAndBalance.text = balance 37 | } 38 | 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/commons/adapters/BankMenuAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.commons.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.mnsons.offlinebank.R 8 | import com.mnsons.offlinebank.model.BankMenuModel 9 | import kotlinx.android.synthetic.main.item_bank.view.* 10 | 11 | class BankMenuAdapter( 12 | private val selectionListener: SelectionListener? = null 13 | ) : RecyclerView.Adapter() { 14 | 15 | var all: List = mutableListOf() 16 | 17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BankMenuItemViewHolder { 18 | return BankMenuItemViewHolder( 19 | LayoutInflater.from(parent.context).inflate(R.layout.item_bank, parent, false) 20 | ) 21 | } 22 | 23 | override fun getItemCount(): Int { 24 | return all.size 25 | } 26 | 27 | override fun onBindViewHolder(holderMenu: BankMenuItemViewHolder, position: Int) { 28 | holderMenu.bind(all[position]) 29 | } 30 | 31 | inner class BankMenuItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 32 | 33 | fun bind(bank: BankMenuModel) { 34 | itemView.tvBankName.text = bank.bankName 35 | itemView.ivBankLogo.setImageResource(R.drawable.ic_bank_logo) 36 | itemView.setOnClickListener { 37 | selectionListener?.select(bank) 38 | } 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/commons/adapters/SelectionListener.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.commons.adapters 2 | 3 | interface SelectionListener { 4 | 5 | fun select(item: T) 6 | 7 | fun deselect(item: T) 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/commons/adapters/bank/BankSelectionAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.commons.adapters.bank 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.core.content.ContextCompat 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.mnsons.offlinebank.R 9 | import com.mnsons.offlinebank.model.bank.BankModel 10 | import com.mnsons.offlinebank.ui.commons.adapters.SelectionListener 11 | import kotlinx.android.synthetic.main.item_bank.view.* 12 | 13 | class BankSelectionAdapter( 14 | private val viewType: ViewType = ViewType.NORMAL, 15 | private val selectionListener: SelectionListener? = null 16 | ) : RecyclerView.Adapter() { 17 | 18 | var all: List = mutableListOf() 19 | var backUp: List = mutableListOf() 20 | var selected: MutableList = mutableListOf() 21 | 22 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BankItemViewHolder { 23 | return BankItemViewHolder( 24 | LayoutInflater.from(parent.context).inflate(R.layout.item_bank, parent, false) 25 | ) 26 | } 27 | 28 | override fun getItemCount(): Int { 29 | return all.size 30 | } 31 | 32 | override fun onBindViewHolder(holder: BankItemViewHolder, position: Int) { 33 | var isSelected = false 34 | 35 | if (selected.contains(all[position])) { 36 | isSelected = true 37 | } 38 | 39 | holder.bind(all[position], isSelected) 40 | } 41 | 42 | fun addToSelected(item: BankModel) { 43 | selected.add(item) 44 | notifyDataSetChanged() 45 | } 46 | 47 | fun removeFromSelected(item: BankModel) { 48 | selected.remove(item) 49 | notifyDataSetChanged() 50 | } 51 | 52 | fun filter(newText: String) { 53 | 54 | } 55 | 56 | inner class BankItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 57 | 58 | fun bind(bank: BankModel, isSelected: Boolean) { 59 | itemView.tvBankName.setText(bank.bankName) 60 | itemView.ivBankLogo.setImageResource(bank.bankLogo) 61 | 62 | if (viewType == ViewType.SELECTABLE) { 63 | itemView.ivCheckMark.isSelected = isSelected 64 | 65 | itemView.itemBankContainer.strokeColor = if (isSelected) { 66 | ContextCompat.getColor(itemView.context, R.color.blue) 67 | } else { 68 | ContextCompat.getColor(itemView.context, R.color.greyTransparent) 69 | } 70 | 71 | itemView.setOnClickListener { 72 | if (isSelected) { 73 | selectionListener?.deselect(bank) 74 | } else { 75 | selectionListener?.select(bank) 76 | } 77 | } 78 | } else { 79 | itemView.ivCheckMark.visibility = View.INVISIBLE 80 | itemView.ivEllipses.visibility = View.VISIBLE 81 | } 82 | 83 | } 84 | } 85 | 86 | enum class ViewType { 87 | NORMAL, 88 | SELECTABLE 89 | } 90 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/commons/adapters/transaction/SectionedTransactionsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.commons.adapters.transaction 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.mnsons.offlinebank.R 8 | import com.mnsons.offlinebank.model.transaction.SectionedTransactionModel 9 | import kotlinx.android.synthetic.main.item_sectioned_transaction.view.* 10 | 11 | class SectionedTransactionsAdapter( 12 | private val transactionsAdapter: TransactionsAdapter 13 | ) : RecyclerView.Adapter() { 14 | 15 | private val sectionedTransactions = mutableListOf() 16 | 17 | override fun onCreateViewHolder( 18 | parent: ViewGroup, 19 | viewType: Int 20 | ): SectionedTransactionItemViewHolder { 21 | return SectionedTransactionItemViewHolder( 22 | LayoutInflater.from(parent.context) 23 | .inflate(R.layout.item_sectioned_transaction, parent, false) 24 | ) 25 | } 26 | 27 | override fun getItemCount(): Int = sectionedTransactions.size 28 | 29 | override fun onBindViewHolder(holder: SectionedTransactionItemViewHolder, position: Int) { 30 | holder.bind(sectionedTransactions[position]) 31 | } 32 | 33 | fun setTransactions(items: List) { 34 | sectionedTransactions.clear() 35 | sectionedTransactions.addAll(items) 36 | notifyDataSetChanged() 37 | } 38 | 39 | inner class SectionedTransactionItemViewHolder(itemView: View) : 40 | RecyclerView.ViewHolder(itemView) { 41 | 42 | fun bind(sectionedTransaction: SectionedTransactionModel) { 43 | itemView.tvDay.text = sectionedTransaction.day 44 | 45 | val totalAmount = sectionedTransaction.transactions.sumByDouble { it.amount } 46 | val transactionsCount = sectionedTransaction.transactions.size 47 | 48 | itemView.tvDescription.text = "You spent N$totalAmount on $transactionsCount Transactions" 49 | 50 | itemView.rvTransactions.adapter = transactionsAdapter 51 | transactionsAdapter.setTransactions(sectionedTransaction.transactions) 52 | } 53 | 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/commons/adapters/transaction/TransactionsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.commons.adapters.transaction 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.mnsons.offlinebank.R 8 | import com.mnsons.offlinebank.model.transaction.TransactionModel 9 | import com.mnsons.offlinebank.utils.TransactionUtil 10 | import kotlinx.android.synthetic.main.item_transaction.view.* 11 | 12 | class TransactionsAdapter : RecyclerView.Adapter() { 13 | 14 | private val transactions = mutableListOf() 15 | 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransactionItemViewHolder { 17 | return TransactionItemViewHolder( 18 | LayoutInflater.from(parent.context).inflate(R.layout.item_transaction, parent, false) 19 | ) 20 | } 21 | 22 | override fun getItemCount(): Int = transactions.size 23 | 24 | override fun onBindViewHolder(holder: TransactionItemViewHolder, position: Int) { 25 | holder.bind(transactions[position]) 26 | } 27 | 28 | fun setTransactions(items: List) { 29 | transactions.clear() 30 | transactions.addAll(items) 31 | notifyDataSetChanged() 32 | } 33 | 34 | inner class TransactionItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 35 | 36 | fun bind(transaction: TransactionModel) { 37 | itemView.ivTypeIcon.setImageResource(TransactionUtil.getIconByType(transaction.type)) 38 | itemView.tvType.text = transaction.type.value 39 | itemView.tvBankName.text = transaction.bank 40 | itemView.tvAmount.text = "N ${transaction.amount}" 41 | } 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/commons/banks/BanksPopulator.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.commons.banks 2 | 3 | import com.mnsons.offlinebank.R 4 | import com.mnsons.offlinebank.model.bank.BankModel 5 | 6 | object BanksPopulator { 7 | 8 | fun fetchSupportedBanks(): List { 9 | val gtBank = BankModel( 10 | R.string.gtbank, 11 | 1, 12 | 0, 13 | "111", 14 | R.drawable.ic_gtbank 15 | ) 16 | 17 | val accessBank = BankModel( 18 | R.string.accessbank, 19 | 2, 20 | 0, 21 | "112", 22 | R.drawable.ic_access 23 | ) 24 | 25 | val zenithBank = BankModel( 26 | R.string.zenith_bank, 27 | 3, 28 | 0, 29 | "113", 30 | R.drawable.ic_zenith 31 | ) 32 | 33 | val uba = BankModel( 34 | R.string.uba, 35 | 4, 36 | 0, 37 | "114", 38 | R.drawable.ic_uba 39 | ) 40 | 41 | return mutableListOf(gtBank, accessBank, zenithBank, uba) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/commons/base/BaseRoundedBottomSheetDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.commons.base 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.annotation.LayoutRes 8 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 9 | import com.mnsons.offlinebank.R 10 | 11 | abstract class BaseRoundedBottomSheetDialogFragment : BottomSheetDialogFragment() { 12 | 13 | override fun getTheme(): Int = R.style.BottomSheetDialogTheme 14 | 15 | override fun onDestroyView() { 16 | if (dialog != null) { 17 | dialog?.setDismissMessage(null) 18 | } 19 | super.onDestroyView() 20 | } 21 | 22 | @LayoutRes 23 | abstract fun getLayoutRes(): Int 24 | 25 | override fun onCreateView( 26 | inflater: LayoutInflater, 27 | container: ViewGroup?, 28 | savedInstanceState: Bundle? 29 | ): View? { 30 | return inflater.inflate(getLayoutRes(), container, false) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/commons/dialogs/SelectBottomSheet.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.commons.dialogs 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import com.mnsons.offlinebank.R 6 | import com.mnsons.offlinebank.model.bank.BankModel 7 | import com.mnsons.offlinebank.ui.commons.adapters.SelectionListener 8 | import com.mnsons.offlinebank.ui.commons.adapters.bank.BankSelectionAdapter 9 | import com.mnsons.offlinebank.ui.commons.base.BaseRoundedBottomSheetDialogFragment 10 | import com.mnsons.offlinebank.utils.ext.slightDelay 11 | import kotlinx.android.synthetic.main.layout_select_bank_bottom_sheet.* 12 | 13 | class SelectBottomSheet( 14 | private val banks: List, 15 | private val selectBankListener: (BankModel) -> Unit 16 | ) : BaseRoundedBottomSheetDialogFragment(), SelectionListener { 17 | 18 | private val bankSelectionAdapter by lazy { 19 | BankSelectionAdapter( 20 | BankSelectionAdapter.ViewType.SELECTABLE, 21 | this 22 | ).apply { 23 | all = banks 24 | } 25 | } 26 | 27 | override fun getLayoutRes(): Int = R.layout.layout_select_bank_bottom_sheet 28 | 29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | 32 | rvBanks.adapter = bankSelectionAdapter 33 | } 34 | 35 | override fun select(item: BankModel) { 36 | dismiss() 37 | 38 | slightDelay({ 39 | selectBankListener.invoke(item) 40 | }, 200) 41 | } 42 | 43 | override fun deselect(item: BankModel) { 44 | 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/commons/dialogs/SelectFromMenuBottomSheet.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.commons.dialogs 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import com.mnsons.offlinebank.R 6 | import com.mnsons.offlinebank.model.BankMenuModel 7 | import com.mnsons.offlinebank.ui.commons.adapters.BankMenuAdapter 8 | import com.mnsons.offlinebank.ui.commons.adapters.SelectionListener 9 | import com.mnsons.offlinebank.ui.commons.base.BaseRoundedBottomSheetDialogFragment 10 | import com.mnsons.offlinebank.utils.ext.slightDelay 11 | import kotlinx.android.synthetic.main.layout_select_bank_bottom_sheet.* 12 | 13 | class SelectFromMenuBottomSheet( 14 | private val banks: List, 15 | private val selectBankListener: (BankMenuModel) -> Unit 16 | ) : BaseRoundedBottomSheetDialogFragment(), SelectionListener { 17 | 18 | private val bankSelectionAdapter by lazy { 19 | BankMenuAdapter( 20 | this 21 | ).apply { 22 | all = banks 23 | } 24 | } 25 | 26 | override fun getLayoutRes(): Int = R.layout.layout_select_bank_bottom_sheet 27 | 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onViewCreated(view, savedInstanceState) 30 | 31 | rvBanks.adapter = bankSelectionAdapter 32 | } 33 | 34 | override fun select(item: BankMenuModel) { 35 | dismiss() 36 | 37 | slightDelay({ 38 | selectBankListener.invoke(item) 39 | }, 200) 40 | } 41 | 42 | override fun deselect(item: BankMenuModel) { 43 | 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/accountbalance/AccountBalanceState.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.accountbalance 2 | 3 | sealed class AccountBalanceState( 4 | val isLoading: Boolean, 5 | val accounts: List?, 6 | val error: Throwable? 7 | ) { 8 | 9 | class Error(error: Throwable?) : AccountBalanceState(false, null, error) 10 | class Fetching : AccountBalanceState(false, null, null) 11 | class Fetched(accounts: List?) : AccountBalanceState(false, accounts, null) 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/accountbalance/AccountBalanceViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.accountbalance 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import com.mnsons.offlinebank.model.bank.BankModel 8 | import javax.inject.Inject 9 | 10 | class AccountBalanceViewModel @ViewModelInject constructor() : ViewModel() { 11 | 12 | lateinit var bank: BankModel 13 | 14 | private val _state = MutableLiveData() 15 | val state: LiveData = _state 16 | 17 | fun fetchAccountBalance() { 18 | _state.value = AccountBalanceState.Fetching() 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/buyairtime/BuyAirtimeState.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.buyairtime 2 | 3 | import com.mnsons.offlinebank.model.buyairtime.BuyAirtimeModel 4 | 5 | sealed class BuyAirtimeState( 6 | val isLoading: Boolean, 7 | val buyAirtimeModel: BuyAirtimeModel?, 8 | val error: Throwable? 9 | ) { 10 | 11 | class Error(error: Throwable?) : BuyAirtimeState(false, null, error) 12 | class Initialize(buyAirtimeModel: BuyAirtimeModel) : 13 | BuyAirtimeState(false, buyAirtimeModel, null) 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/buyairtime/BuyAirtimeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.buyairtime 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.mnsons.offlinebank.data.cache.impl.TransactionsCache 9 | import com.mnsons.offlinebank.model.bank.BankModel 10 | import com.mnsons.offlinebank.model.buyairtime.BuyAirtimeModel 11 | import com.mnsons.offlinebank.model.transaction.TransactionModel 12 | import com.mnsons.offlinebank.model.transaction.TransactionStatus 13 | import com.mnsons.offlinebank.model.transaction.TransactionType 14 | import kotlinx.coroutines.launch 15 | import java.util.* 16 | import javax.inject.Inject 17 | 18 | class BuyAirtimeViewModel @ViewModelInject constructor(private val transactionsCache: TransactionsCache) : 19 | ViewModel() { 20 | 21 | lateinit var bank: BankModel 22 | 23 | private var buyAirtimeModel = BuyAirtimeModel() 24 | private val _state = MutableLiveData() 25 | val state: LiveData = _state 26 | 27 | 28 | fun persistTransaction(status: TransactionStatus) { 29 | viewModelScope.launch { 30 | transactionsCache.saveTransaction( 31 | TransactionModel(buyAirtimeModel.amount!!.toDouble(), Calendar.getInstance().time.time, 32 | TransactionType.AIRTIME_PURCHASE, status, buyAirtimeModel.bank!!) 33 | ) 34 | } 35 | } 36 | 37 | fun initiateBuyAirtime(actionId: String, amount: String, phoneNumber: String, originBankAccount:String) { 38 | when { 39 | amount.isEmpty() -> { 40 | _state.value = BuyAirtimeState.Error(Throwable("Please input an amount to buy")) 41 | } 42 | phoneNumber.isEmpty() -> { 43 | _state.value = 44 | BuyAirtimeState.Error(Throwable("Please input recipient's phone number")) 45 | } 46 | else -> { 47 | buyAirtimeModel = BuyAirtimeModel( 48 | actionId, 49 | amount, 50 | phoneNumber, 51 | originBankAccount 52 | ) 53 | _state.value = BuyAirtimeState.Initialize( 54 | buyAirtimeModel 55 | ) 56 | } 57 | } 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/dashboard/DashboardFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.dashboard 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.fragment.app.Fragment 10 | import androidx.fragment.app.viewModels 11 | import com.mnsons.offlinebank.R 12 | import com.mnsons.offlinebank.databinding.FragmentDashboardBinding 13 | import com.mnsons.offlinebank.model.transaction.TransactionType 14 | import com.mnsons.offlinebank.ui.commons.adapters.transaction.SectionedTransactionsAdapter 15 | import com.mnsons.offlinebank.ui.commons.adapters.transaction.TransactionsAdapter 16 | import com.mnsons.offlinebank.ui.main.MainActivity 17 | import com.mnsons.offlinebank.utils.ext.nonNullObserve 18 | import com.mnsons.offlinebank.utils.ext.showSnackbar 19 | import com.mnsons.offlinebank.utils.ext.viewBinding 20 | import dagger.hilt.android.AndroidEntryPoint 21 | import javax.inject.Inject 22 | 23 | @AndroidEntryPoint 24 | class DashboardFragment : Fragment(R.layout.fragment_dashboard) { 25 | 26 | private val dashboardViewModel: DashboardViewModel by viewModels() 27 | 28 | private val transactionsAdapter by lazy { TransactionsAdapter() } 29 | 30 | private val sectionedTransactionsAdapter by lazy { 31 | SectionedTransactionsAdapter(transactionsAdapter) 32 | } 33 | 34 | private val binding: FragmentDashboardBinding by viewBinding(FragmentDashboardBinding::bind) 35 | 36 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 37 | super.onViewCreated(view, savedInstanceState) 38 | 39 | binding.rvSectionedTransactions.adapter = sectionedTransactionsAdapter 40 | 41 | nonNullObserve(dashboardViewModel.state, ::handleStates) 42 | } 43 | 44 | @SuppressLint("SetTextI18n") 45 | private fun handleStates(dashboardState: DashboardState) { 46 | when (dashboardState) { 47 | is DashboardState.Idle, is DashboardState.Editing -> { 48 | dashboardState.transactions?.let { 49 | sectionedTransactionsAdapter.setTransactions(it) 50 | 51 | val allTransactions = it.flatMap { it.transactions } 52 | 53 | binding.tvBankTransferAmount.text = "N " + allTransactions.filter { 54 | it.type == TransactionType.BANK_TRANSFER 55 | }.sumByDouble { it.amount }.toString() 56 | 57 | binding.tvAirtimePurchaseAmount.text = "N " + allTransactions.filter { 58 | it.type == TransactionType.AIRTIME_PURCHASE 59 | }.sumByDouble { it.amount }.toString() 60 | } 61 | } 62 | is DashboardState.Error -> showSnackbar(dashboardState.error?.message.toString()) 63 | } 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/dashboard/DashboardState.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.dashboard 2 | 3 | import com.mnsons.offlinebank.model.transaction.SectionedTransactionModel 4 | 5 | sealed class DashboardState( 6 | val isLoading: Boolean, 7 | val transactions: List?, 8 | val error: Throwable? 9 | ) { 10 | 11 | class Error(error: Throwable?) : DashboardState(false, null, error) 12 | class Editing(transactions: List) : 13 | DashboardState(false, transactions, null) 14 | class Idle(transactions: List) : 15 | DashboardState(false, transactions, null) 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/dashboard/DashboardViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.dashboard 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.mnsons.offlinebank.data.cache.impl.TransactionsCache 9 | import com.mnsons.offlinebank.model.transaction.SectionedTransactionModel 10 | import com.mnsons.offlinebank.model.transaction.TransactionModel 11 | import kotlinx.coroutines.flow.launchIn 12 | import kotlinx.coroutines.flow.onEach 13 | import java.text.SimpleDateFormat 14 | import java.util.* 15 | import javax.inject.Inject 16 | 17 | class DashboardViewModel @ViewModelInject constructor( 18 | transactionsCache: TransactionsCache 19 | ) : ViewModel() { 20 | 21 | private val _state = MutableLiveData() 22 | val state: LiveData = _state 23 | 24 | init { 25 | transactionsCache.getTransaction().onEach { 26 | _state.value = DashboardState.Idle(generateSectionedTransactions(it)) 27 | }.launchIn(viewModelScope) 28 | } 29 | 30 | private fun generateSectionedTransactions( 31 | transactions: List 32 | ): List { 33 | val sectionedTransactions = mutableListOf() 34 | val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) 35 | 36 | val sortedTransactions = transactions.sortedByDescending { it.timestamp } 37 | 38 | sortedTransactions.forEach { transaction -> 39 | val dayString = 40 | when (val formattedDateString = dateFormat.format(Date(transaction.timestamp))) { 41 | dateFormat.format(Date()) -> "Today" 42 | dateFormat.format(Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000)) -> "Yesterday" 43 | else -> formattedDateString 44 | } 45 | 46 | val dayList = sectionedTransactions.map { it.day } 47 | 48 | if (dayString in dayList) { 49 | sectionedTransactions.find { 50 | it.day == dayString 51 | }?.transactions?.add(transaction) 52 | } else { 53 | sectionedTransactions.add( 54 | SectionedTransactionModel(dayString, mutableListOf(transaction)) 55 | ) 56 | } 57 | } 58 | 59 | return sectionedTransactions 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.home 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | 7 | class HomeViewModel : ViewModel() { 8 | 9 | private val _text = MutableLiveData().apply { 10 | value = "This is home Fragment" 11 | } 12 | val text: LiveData = _text 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/home/menu/MenuAction.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.home.menu 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | import com.mnsons.offlinebank.R 6 | 7 | sealed class MenuAction( 8 | val id: Int, 9 | @StringRes val titleRes: Int, 10 | @DrawableRes val iconRes: Int, 11 | val isEnabled: Boolean = false 12 | ) { 13 | object TransferFunds : MenuAction(1, R.string.transfer_funds, R.drawable.transfer, true) 14 | object BuyAirtime : MenuAction(2, R.string.buy_airtime, R.drawable.ic_airtime, true) 15 | object BuyData : MenuAction(3, R.string.buy_internet, R.drawable.ic_internet, true) 16 | object PayBills : MenuAction(4, R.string.pay_bills, R.drawable.ic_bills, false) 17 | object CheckAccountBalance : 18 | MenuAction(5, R.string.check_balance, R.drawable.ic_profile_circle, true) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/home/menu/MenuAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.home.menu 2 | 3 | import android.text.Html 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.ListAdapter 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.mnsons.offlinebank.databinding.MenuItemCardBinding 10 | import com.mnsons.offlinebank.utils.ext.recursivelyApplyToChildren 11 | 12 | class MenuAdapter constructor(private val menuActionClickListener: MenuActionClickListener) : 13 | ListAdapter(DiffCallback()) { 14 | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 16 | val binding = 17 | MenuItemCardBinding.inflate(LayoutInflater.from(parent.context), parent, false) 18 | return MenuActionViewHolder(binding, menuActionClickListener) 19 | } 20 | 21 | fun isEmpty() = super.getItemCount() == 0 22 | 23 | 24 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 25 | (holder as MenuActionViewHolder).bind(getItem(position)) 26 | } 27 | 28 | 29 | class MenuActionViewHolder( 30 | private val binding: MenuItemCardBinding, 31 | private val actionClickListener: MenuActionClickListener 32 | ) : 33 | RecyclerView.ViewHolder(binding.root) { 34 | 35 | fun bind(model: MenuAction) { 36 | itemView.setOnClickListener { 37 | actionClickListener.onMenuActionClick(model) 38 | } 39 | binding.parent.recursivelyApplyToChildren { 40 | it.isEnabled = model.isEnabled 41 | } 42 | binding.icon.setImageDrawable(itemView.context.getDrawable(model.iconRes)) 43 | binding.title.text = Html.fromHtml(itemView.context.getString(model.titleRes)) 44 | } 45 | 46 | } 47 | 48 | class DiffCallback : DiffUtil.ItemCallback() { 49 | override fun areItemsTheSame(oldItem: MenuAction, newItem: MenuAction): Boolean { 50 | return oldItem.id == newItem.id 51 | } 52 | 53 | override fun areContentsTheSame( 54 | oldItem: MenuAction, 55 | newItem: MenuAction 56 | ): Boolean { 57 | return oldItem == newItem 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/home/menu/MenuItemClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.home.menu 2 | 3 | 4 | interface MenuActionClickListener { 5 | fun onMenuActionClick(model: MenuAction) 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/presentation/MainState.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.presentation 2 | 3 | import com.mnsons.offlinebank.model.user.UserModel 4 | 5 | sealed class MainState( 6 | val isLoading: Boolean, 7 | val user: UserModel?, 8 | val error: Throwable? 9 | ) { 10 | 11 | object LoggedOut : MainState(false, null, null) 12 | class Idle(user: UserModel) : MainState(false, user, null) 13 | class Editing(user: UserModel) : MainState(false, user, null) 14 | class Error(error: Throwable?) : MainState(false, null, error) 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/presentation/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.presentation 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.mnsons.offlinebank.data.cache.impl.BanksCache 9 | import com.mnsons.offlinebank.data.cache.impl.SettingsCache 10 | import com.mnsons.offlinebank.model.* 11 | import com.mnsons.offlinebank.model.user.UserModel 12 | import kotlinx.coroutines.flow.launchIn 13 | import kotlinx.coroutines.flow.onEach 14 | 15 | class MainViewModel @ViewModelInject constructor( 16 | private val settingsCache: SettingsCache, 17 | private val banksCache: BanksCache 18 | ) : ViewModel() { 19 | 20 | private val _state = MutableLiveData() 21 | val state: LiveData = _state 22 | 23 | init { 24 | if (settingsCache.userDataExists()) { 25 | banksCache.getBanks() 26 | .onEach { banks -> 27 | _state.value = MainState.Idle( 28 | UserModel( 29 | settingsCache.fetchUserFirstName()!!, 30 | settingsCache.fetchUserLastName()!!, 31 | settingsCache.fetchUserPhone()!!, 32 | banks.mapInto { 33 | it.toBankModel() 34 | } 35 | ) 36 | ) 37 | } 38 | .launchIn(viewModelScope) 39 | } else { 40 | _state.value = MainState.LoggedOut 41 | } 42 | } 43 | 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/profile/addbank/AddBankState.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.profile.addbank 2 | 3 | import com.mnsons.offlinebank.model.user.UserModel 4 | 5 | sealed class AddBankState( 6 | val isLoading: Boolean, 7 | val user: UserModel?, 8 | val error: Throwable? 9 | ) { 10 | 11 | class Idle(user: UserModel) : AddBankState(false, user, null) 12 | class Editing(user: UserModel) : AddBankState(false, user, null) 13 | class Error(error: Throwable?) : AddBankState(false, null, error) 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/profile/addbank/AddBankViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.profile.addbank 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.mnsons.offlinebank.data.cache.impl.BanksCache 9 | import com.mnsons.offlinebank.data.cache.impl.SettingsCache 10 | import com.mnsons.offlinebank.model.* 11 | import com.mnsons.offlinebank.model.bank.BankModel 12 | import com.mnsons.offlinebank.model.user.UserModel 13 | import kotlinx.coroutines.flow.launchIn 14 | import kotlinx.coroutines.flow.onEach 15 | import kotlinx.coroutines.launch 16 | import javax.inject.Inject 17 | 18 | class AddBankViewModel @ViewModelInject constructor( 19 | val settingsCache: SettingsCache, 20 | val banksCache: BanksCache 21 | ) : ViewModel() { 22 | 23 | private val _state = MutableLiveData() 24 | val state: LiveData = _state 25 | 26 | init { 27 | if (settingsCache.userDataExists()) { 28 | banksCache.getBanks() 29 | .onEach { banks -> 30 | _state.value = AddBankState.Idle( 31 | UserModel( 32 | settingsCache.fetchUserFirstName()!!, 33 | settingsCache.fetchUserLastName()!!, 34 | settingsCache.fetchUserPhone()!!, 35 | banks.mapInto { 36 | it.toBankModel() 37 | } 38 | ) 39 | ) 40 | }.launchIn(viewModelScope) 41 | } 42 | } 43 | 44 | fun updateUserBanks(banks: List) { 45 | if (banks.isEmpty()) { 46 | _state.value = AddBankState.Error(Throwable("Please select at least one bank")) 47 | } else { 48 | if (settingsCache.userDataExists()) { 49 | _state.value = AddBankState.Editing( 50 | UserModel( 51 | settingsCache.fetchUserFirstName()!!, 52 | settingsCache.fetchUserLastName()!!, 53 | settingsCache.fetchUserPhone()!!, 54 | banks 55 | ) 56 | ) 57 | 58 | viewModelScope.launch { 59 | banksCache.clearBanks() 60 | banksCache.saveBanks( 61 | banks.mapInto { 62 | it.toBankCacheModel() 63 | } 64 | ) 65 | } 66 | } else { 67 | _state.value = AddBankState.Error(Throwable("There was an error adding new banks")) 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/profile/userdetails/UserDetailsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.profile.userdetails 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.fragment.app.activityViewModels 7 | import androidx.navigation.fragment.findNavController 8 | import com.mnsons.offlinebank.R 9 | import com.mnsons.offlinebank.databinding.FragmentUserDetailsBinding 10 | import com.mnsons.offlinebank.ui.commons.adapters.bank.BankSelectionAdapter 11 | import com.mnsons.offlinebank.ui.main.presentation.MainState 12 | import com.mnsons.offlinebank.ui.main.presentation.MainViewModel 13 | import com.mnsons.offlinebank.utils.ext.nonNullObserve 14 | import com.mnsons.offlinebank.utils.ext.viewBinding 15 | import dagger.hilt.android.AndroidEntryPoint 16 | 17 | @AndroidEntryPoint 18 | class UserDetailsFragment : Fragment(R.layout.fragment_user_details) { 19 | 20 | private val binding: FragmentUserDetailsBinding by viewBinding(FragmentUserDetailsBinding::bind) 21 | 22 | private val mainViewModel: MainViewModel by activityViewModels() 23 | 24 | private val bankSelectionAdapter by lazy { 25 | BankSelectionAdapter() 26 | } 27 | 28 | 29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | 32 | nonNullObserve(mainViewModel.state, ::handleStates) 33 | 34 | binding.btnAddBank.setOnClickListener { 35 | findNavController().navigate(UserDetailsFragmentDirections.actionNavigationProfileToNavigationAddBank()) 36 | } 37 | } 38 | 39 | 40 | private fun handleStates(mainState: MainState) { 41 | if (mainState is MainState.Idle) { 42 | mainState.user?.let { 43 | binding.tvUserFullName.text = "${it.firstName} ${it.lastName}" 44 | binding.tvUserPhoneNumber.text = it.phoneNumber 45 | bankSelectionAdapter.all = it.banks 46 | binding.rvSelectedBanks.adapter = bankSelectionAdapter 47 | } 48 | } 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/transfermoney/TransferMoneyState.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.transfermoney 2 | 3 | import com.mnsons.offlinebank.model.MoneyTransferModel 4 | 5 | sealed class TransferMoneyState( 6 | val transferModel: MoneyTransferModel?, 7 | val error: Throwable? 8 | ) { 9 | 10 | class Error(error: Throwable?) : TransferMoneyState(null, error) 11 | class Initialize(moneyTransferModel: MoneyTransferModel) : 12 | TransferMoneyState(moneyTransferModel, null) 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/main/transfermoney/TransferMoneyViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.main.transfermoney 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.mnsons.offlinebank.data.cache.impl.BanksCache 9 | import com.mnsons.offlinebank.data.cache.impl.TransactionsCache 10 | import com.mnsons.offlinebank.model.BankMenuModel 11 | import com.mnsons.offlinebank.model.MoneyTransferModel 12 | import com.mnsons.offlinebank.model.transaction.TransactionModel 13 | import com.mnsons.offlinebank.model.transaction.TransactionStatus 14 | import com.mnsons.offlinebank.model.transaction.TransactionType 15 | import kotlinx.coroutines.flow.launchIn 16 | import kotlinx.coroutines.flow.onEach 17 | import kotlinx.coroutines.launch 18 | import java.util.* 19 | 20 | class TransferMoneyViewModel @ViewModelInject constructor( 21 | private val bankCache: BanksCache, 22 | private val transactionsCache: TransactionsCache 23 | ) : ViewModel() { 24 | 25 | var bankIds = emptyList() 26 | 27 | private val _state = MutableLiveData() 28 | val state: LiveData = _state 29 | 30 | private var moneyTransferModel: MoneyTransferModel = MoneyTransferModel() 31 | 32 | fun fetchBankMenu(bankId: Int) { 33 | bankCache.getBankMenu(bankId) 34 | .onEach { 35 | bankIds = it 36 | }.launchIn(viewModelScope) 37 | } 38 | 39 | 40 | fun persistTransaction(status: TransactionStatus) { 41 | viewModelScope.launch { 42 | transactionsCache.saveTransaction(TransactionModel(moneyTransferModel.amount!!.toDouble(), 43 | Calendar.getInstance().time.time, TransactionType.BANK_TRANSFER, 44 | status, moneyTransferModel.bank!!)) 45 | } 46 | } 47 | 48 | fun initiateFundTransfer(actionId: String, amount: String, accountNumber: String, originatingBankName:String) { 49 | when { 50 | amount.isEmpty() -> { 51 | _state.value = TransferMoneyState.Error(Throwable("Please input an amount to buy")) 52 | } 53 | accountNumber.isEmpty() -> { 54 | _state.value = 55 | TransferMoneyState.Error(Throwable("Please input recipient's bank account")) 56 | } 57 | else -> { 58 | moneyTransferModel = moneyTransferModel.copy( 59 | actionId = actionId, 60 | amount = amount, 61 | accountNumber = accountNumber, 62 | bank = originatingBankName 63 | ) 64 | _state.value = TransferMoneyState.Initialize(moneyTransferModel) 65 | } 66 | } 67 | } 68 | 69 | fun saveTransaction() { 70 | 71 | } 72 | 73 | fun setRecipientBank(id: Int) { 74 | moneyTransferModel = moneyTransferModel.copy(recipientBank = id) 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/onboarding/OnBoardingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.onboarding 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import com.mnsons.offlinebank.R 6 | import dagger.hilt.android.AndroidEntryPoint 7 | 8 | @AndroidEntryPoint 9 | class OnBoardingActivity : AppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.activity_on_boarding) 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/onboarding/collectuserdetails/CollectUserDetailsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.onboarding.collectuserdetails 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.viewModels 10 | import androidx.navigation.fragment.findNavController 11 | import com.google.android.material.snackbar.Snackbar 12 | import com.mnsons.offlinebank.R 13 | import com.mnsons.offlinebank.databinding.FragmentCollectUserDetailsBinding 14 | import com.mnsons.offlinebank.ui.onboarding.OnBoardingActivity 15 | import com.mnsons.offlinebank.ui.onboarding.presentation.OnBoardingState 16 | import com.mnsons.offlinebank.ui.onboarding.presentation.OnBoardingViewModel 17 | import com.mnsons.offlinebank.utils.ext.nonNullObserve 18 | import com.mnsons.offlinebank.utils.ext.viewBinding 19 | import dagger.hilt.android.AndroidEntryPoint 20 | 21 | @AndroidEntryPoint 22 | class CollectUserDetailsFragment : Fragment(R.layout.fragment_collect_user_details) { 23 | 24 | private val onBoardingViewModel: OnBoardingViewModel by viewModels() 25 | 26 | private val binding: FragmentCollectUserDetailsBinding by viewBinding(FragmentCollectUserDetailsBinding::bind) 27 | 28 | 29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | 32 | binding.btnNext.setOnClickListener { 33 | onBoardingViewModel.setUserDetails( 34 | binding.etFirstName.text.toString(), 35 | binding.etLastName.text.toString(), 36 | binding.etPhoneNumber.text.toString() 37 | ) 38 | } 39 | 40 | nonNullObserve(onBoardingViewModel.state, ::handleState) 41 | } 42 | 43 | 44 | private fun handleState(onBoardingState: OnBoardingState) { 45 | when (onBoardingState) { 46 | is OnBoardingState.Loading -> { 47 | 48 | } 49 | is OnBoardingState.Finished -> { 50 | 51 | } 52 | is OnBoardingState.Editing -> { 53 | findNavController().navigate(R.id.action_navigation_collect_user_details_to_navigation_select_user_banks) 54 | } 55 | is OnBoardingState.Error -> { 56 | Snackbar.make( 57 | binding.root, 58 | onBoardingState.error?.message.toString(), 59 | Snackbar.LENGTH_LONG 60 | ).show() 61 | } 62 | } 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/onboarding/done/AllDoneFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.onboarding.done 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import com.mnsons.offlinebank.R 9 | 10 | class AllDoneFragment : Fragment() { 11 | 12 | override fun onCreateView( 13 | inflater: LayoutInflater, container: ViewGroup?, 14 | savedInstanceState: Bundle? 15 | ): View? { 16 | return inflater.inflate(R.layout.fragment_all_done, container, false) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/onboarding/presentation/OnBoardingState.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.onboarding.presentation 2 | 3 | import com.mnsons.offlinebank.model.user.UserModel 4 | 5 | sealed class OnBoardingState( 6 | val isLoading: Boolean, 7 | val user: UserModel?, 8 | val error: Throwable? 9 | ) { 10 | class Editing(user: UserModel) : OnBoardingState(false, user, null) 11 | class Error(error: Throwable?) : OnBoardingState(false, null, error) 12 | class Loading(isLoading: Boolean) : OnBoardingState(isLoading, null, null) 13 | class Finished(user: UserModel) : OnBoardingState(false, user, null) 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/onboarding/selectuserbanks/BankTransferMenuIndexer.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.onboarding.selectuserbanks 2 | 3 | import androidx.fragment.app.Fragment 4 | import com.mnsons.offlinebank.contracts.access.FetchAccessBankOtherBanksContract 5 | import com.mnsons.offlinebank.contracts.gtb.FetchGTBankOtherBanksFirstPageContract 6 | import com.mnsons.offlinebank.model.BankMenuModel 7 | import com.mnsons.offlinebank.model.bank.BankModel 8 | import javax.inject.Inject 9 | 10 | class BankTransferMenuIndexer @Inject constructor( 11 | private val successAction: (bankId: Int, List) -> Unit, 12 | private val finishedAction: () -> Unit, 13 | val fragment: Fragment 14 | ) { 15 | 16 | private var selectedBanks = listOf() 17 | private var currentIndex: Int = 0 18 | 19 | private val gtBankMenuCall = 20 | fragment.registerForActivityResult(FetchGTBankOtherBanksFirstPageContract()) { result -> 21 | result.data?.let { 22 | successAction.invoke(selectedBanks[currentIndex].id, it) 23 | } 24 | runNextMenuCall() 25 | } 26 | 27 | private val accessBankMenuCall = fragment.registerForActivityResult( 28 | FetchAccessBankOtherBanksContract() 29 | ) { result -> 30 | result.data?.let { 31 | successAction.invoke(selectedBanks[currentIndex].id, it) 32 | } 33 | runNextMenuCall() 34 | } 35 | 36 | private fun runNextMenuCall() { 37 | currentIndex += 1 38 | if (currentIndex <= selectedBanks.lastIndex) { 39 | runContractForBank(selectedBanks[currentIndex]) 40 | } else { 41 | currentIndex = 0 42 | selectedBanks = emptyList() 43 | finishedAction.invoke() 44 | } 45 | } 46 | 47 | private fun runContractForBank(bankModel: BankModel) { 48 | if (bankModel.id == 1) { 49 | gtBankMenuCall.launch(Unit) 50 | } else if (bankModel.id == 2) { 51 | accessBankMenuCall.launch(Unit) 52 | } 53 | } 54 | 55 | fun indexMenuForBanks(selectedBanks: List) { 56 | this.selectedBanks = selectedBanks 57 | runContractForBank(this.selectedBanks[0]) 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/ui/onboarding/setingup/SettingUpFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.ui.onboarding.setingup 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import com.mnsons.offlinebank.R 9 | 10 | class SettingUpFragment : Fragment() { 11 | 12 | override fun onCreateView( 13 | inflater: LayoutInflater, container: ViewGroup?, 14 | savedInstanceState: Bundle? 15 | ): View? { 16 | return inflater.inflate(R.layout.fragment_setting_up, container, false) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/utils/BuyAirtimeUtil.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.utils 2 | 3 | object BuyAirtimeUtil { 4 | 5 | fun getActionIdByBankId(id: Int): String = hashMapOfActions[id]!! 6 | 7 | private val hashMapOfActions = hashMapOf( 8 | 1 to "58f263f5", 9 | 2 to "1fb3b56e" 10 | ) 11 | 12 | } 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/utils/CheckBalanceUtil.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.utils 2 | 3 | object CheckBalanceUtil { 4 | 5 | fun getActionIdByBankId(id: Int) = hashMapOfActions[id] 6 | 7 | private val hashMapOfActions = hashMapOf( 8 | 1 to "19142a42" 9 | ) 10 | } 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.utils 2 | 3 | object Constants { 4 | 5 | const val EXTRA_PHONE_NUMBER = "phoneNumber" 6 | const val EXTRA_AMOUNT = "amount" 7 | 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/utils/TransferMoneyUtil.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.utils 2 | 3 | object TransferMoneyUtil { 4 | 5 | fun getActionIdByBankId(id: Int): String = hashMapOfActions[id]!! 6 | 7 | private val hashMapOfActions = hashMapOf( 8 | 1 to "75872765", 9 | 2 to "f561a10f" 10 | ) 11 | 12 | } 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/utils/ext/ContextExtensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.utils.ext 17 | 18 | import android.content.Context 19 | import android.util.DisplayMetrics 20 | import androidx.annotation.ColorRes 21 | import androidx.annotation.StringRes 22 | import androidx.core.content.ContextCompat 23 | import kotlin.math.roundToInt 24 | 25 | /** 26 | * Get resource string from optional id 27 | * 28 | * @param resId Resource string identifier. 29 | * @return The key value if exist, otherwise empty. 30 | */ 31 | fun Context.getString(@StringRes resId: Int?) = 32 | resId?.let { 33 | getString(it) 34 | } ?: run { 35 | "" 36 | } 37 | 38 | 39 | fun Context.dpToPx(dp: Int): Int { 40 | var displayMetrics = resources.displayMetrics 41 | return (dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)).roundToInt() 42 | } 43 | 44 | fun Context.getColorHexString(@ColorRes resId: Int): String { 45 | val colorInt = ContextCompat.getColor(this, resId) 46 | return String.format("#%06X", 0xFFFFFF and colorInt) 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/utils/ext/LifecycleOwnerExtensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.utils.ext 17 | 18 | import androidx.lifecycle.LifecycleOwner 19 | import androidx.lifecycle.LiveData 20 | import androidx.lifecycle.MutableLiveData 21 | import androidx.lifecycle.Observer 22 | import com.mnsons.offlinebank.utils.livedata.NonNullObserver 23 | 24 | /** 25 | * Adds the given observer to the observers list within the lifespan of the given 26 | * owner. The events are dispatched on the main_home thread. If LiveData already has data 27 | * set, it will be delivered to the observer. 28 | * 29 | * @param liveData The liveData to observe. 30 | * @param observer The observer that will receive the events. 31 | * @see LiveData.observe 32 | */ 33 | fun LifecycleOwner.observe(liveData: LiveData, observer: (T) -> Unit) { 34 | liveData.observe(this, Observer { 35 | it?.let { t -> observer(t) } 36 | }) 37 | } 38 | 39 | fun > LifecycleOwner.nonNullObserve(liveData: L, body: (T) -> Unit) { 40 | liveData.observe(this, NonNullObserver(body)) 41 | } 42 | 43 | /** 44 | * Adds the given observer to the observers list within the lifespan of the given 45 | * owner. The events are dispatched on the main_home thread. If LiveData already has data 46 | * set, it will be delivered to the observer. 47 | * 48 | * @param liveData The mutableLiveData to observe. 49 | * @param observer The observer that will receive the events. 50 | * @see MutableLiveData.observe 51 | */ 52 | fun LifecycleOwner.observe(liveData: MutableLiveData, observer: (T) -> Unit) { 53 | liveData.observe(this, Observer { 54 | it?.let { t -> observer(t) } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/utils/ext/ListExt.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.utils.ext 17 | 18 | 19 | fun List.replaceItemInList(comparator: (item: T) -> Boolean, item: T): List { 20 | val list = this.toMutableList() 21 | list.forEachIndexed { index, each -> 22 | each.takeIf { comparator(each) }?.let { 23 | list[index] = item 24 | } 25 | } 26 | return list 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/utils/ext/LiveDataExtension.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.utils.ext 17 | 18 | import androidx.lifecycle.LifecycleOwner 19 | import androidx.lifecycle.LiveData 20 | import androidx.lifecycle.Observer 21 | import androidx.lifecycle.Transformations 22 | 23 | fun LiveData.map(transformation: (T) -> R): LiveData { 24 | return Transformations.map(this, transformation) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/utils/ext/StringExt.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.utils.ext 17 | 18 | import android.os.Build 19 | import android.os.Build.VERSION.SDK_INT 20 | import android.text.Html 21 | import android.text.Spanned 22 | 23 | 24 | @SuppressWarnings("deprecation") 25 | fun fromHtml(source: String?): Spanned { 26 | return if (SDK_INT >= Build.VERSION_CODES.N) { 27 | Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY) 28 | } else { 29 | Html.fromHtml(source) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/utils/flow/PostExecutionThread.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.utils.flow 17 | 18 | import kotlinx.coroutines.CoroutineDispatcher 19 | 20 | 21 | interface PostExecutionThread { 22 | 23 | val ui: CoroutineDispatcher 24 | 25 | val io: CoroutineDispatcher 26 | 27 | val default: CoroutineDispatcher 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/utils/flow/PostExecutionThreadImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Abdul-Mujeeb Aliu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mnsons.offlinebank.utils.flow 17 | 18 | import kotlinx.coroutines.CoroutineDispatcher 19 | import kotlinx.coroutines.Dispatchers 20 | import javax.inject.Inject 21 | 22 | class PostExecutionThreadImpl @Inject constructor() : PostExecutionThread { 23 | 24 | override val ui: CoroutineDispatcher = Dispatchers.Main 25 | override val io: CoroutineDispatcher = Dispatchers.IO 26 | override val default: CoroutineDispatcher = Dispatchers.Default 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/mnsons/offlinebank/utils/livedata/NonNullObserver.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank.utils.livedata 2 | 3 | import androidx.lifecycle.Observer 4 | 5 | 6 | internal class NonNullObserver(private val block: (T) -> Unit) : Observer { 7 | 8 | override fun onChanged(it: T?) { 9 | if (it != null) { 10 | block(it) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/activity.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bottom_sheet_background_drawable.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/home.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_access.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_airtime.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_airtime_purchase_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_airtime_purchase_summary.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bank_logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bank_not_selected.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bank_selected.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bank_transfer_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bank_transfer_summary.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_arrow_back_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dashboard_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dummy_bank_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/drawable/ic_dummy_bank_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dummy_user_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/drawable/ic_dummy_user_avatar.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_ellipses.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_gtbank.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_indicator.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_internet.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_item_bank_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notifications_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_official_building.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_profile_circle.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_transaction_failure.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_transaction_success.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zenith.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 13 | 17 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/profile.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/transfer.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/font/maisonneue_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/font/maisonneue_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/maisonneue_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/font/maisonneue_bold_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/maisonneue_book.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/font/maisonneue_book.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/maisonneue_book_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/font/maisonneue_book_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/maisonneue_demi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/font/maisonneue_demi.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/maisonneue_demi_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/font/maisonneue_demi_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/maisonneue_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/font/maisonneue_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/maisonneue_light_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/font/maisonneue_light_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/maisonneue_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/font/maisonneue_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/maisonneue_medium_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/font/maisonneue_medium_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 28 | 29 | 30 | 42 | 43 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_on_boarding.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_account_balance.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | 23 | 35 | 36 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_add_bank.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 23 | 24 | 32 | 33 | 41 | 42 | 43 | 58 | 59 | 68 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_all_done.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 30 | 31 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_buy_airtime.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_notifications.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_setting_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_transfer_money.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 15 | 16 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_account_balance.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_bank.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 18 | 19 | 28 | 29 | 42 | 43 | 52 | 53 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_sectioned_transaction.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 30 | 31 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_transaction.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 31 | 32 | 42 | 43 | 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_buy_airtime_details.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 47 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_enter_ussd_pin.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 20 | 21 | 31 | 32 | 39 | 40 | 41 | 50 | 51 | 69 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_select_bank.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 22 | 31 | 32 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_select_bank_bottom_sheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 23 | 24 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_successful_transaction.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 22 | 23 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_transaction_outcome.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 17 | 18 | 34 | 35 | 45 | 46 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/res/layout/menu_item_card.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 18 | 19 | 27 | 28 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bottom_nav_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 13 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/onboarding_navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 21 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values-v23/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #BB86FC 5 | #6200EE 6 | #3700B3 7 | #03DAC5 8 | 9 | #3274F5 10 | #5F6578 11 | #C7C7C7 12 | #FFFFFF 13 | #48FFFFFF 14 | 15 | #E8E8E8 16 | #165F6578 17 | #000000 18 | 19 | #34C7C7C7 20 | #E8E8E8 21 | 22 | #1AE1E1E1 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 170dp 6 | 150dp 7 | 18sp 8 | 9 | 10 | 32dp 11 | 8dp 12 | 16dp 13 | 24dp 14 | 4dp 15 | 16sp 16 | 12dp 17 | 60dp 18 | 24dp 19 | -------------------------------------------------------------------------------- /app/src/test/java/com/mnsons/offlinebank/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.mnsons.offlinebank 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/app_icon.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | 4 | //this is how we manage dependencies for now. KISS 5 | ext.kotlin_version = "1.4.10" 6 | ext.activity_version = "1.2.0-alpha04" 7 | ext.fragment_version = "1.3.0-alpha04" 8 | ext.room_version = "2.2.5" 9 | ext.coroutines = "1.3.9" 10 | ext.flowbinding_version = "1.0.0-alpha02" 11 | ext.hilt_version = "2.28-alpha" 12 | ext.coil_version = "1.0.0-rc3" 13 | ext.kohii_version = "1.1.1.2011003" 14 | ext.navigation_version = "2.3.0" 15 | ext.dagger = "2.27" 16 | 17 | repositories { 18 | google() 19 | jcenter() 20 | maven {url 'https://jitpack.io'} 21 | } 22 | dependencies { 23 | classpath 'com.android.tools.build:gradle:4.2.0-alpha15' 24 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 25 | classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version" 26 | classpath 'com.google.gms:google-services:4.3.4' 27 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0' 28 | classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" 29 | classpath "com.github.alexfu:androidautoversion:3.3.0" 30 | 31 | // NOTE: Do not place your application dependencies here; they belong 32 | // in the individual module build.gradle files 33 | } 34 | } 35 | 36 | allprojects { 37 | repositories { 38 | google() 39 | mavenCentral() 40 | maven { url "https://jitpack.io" } 41 | maven { url "http://maven.usehover.com/releases" } 42 | jcenter() 43 | } 44 | 45 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 46 | kotlinOptions { 47 | freeCompilerArgs = ['-Xjvm-default=enable'] //enable or compatibility 48 | jvmTarget = "1.8" 49 | } 50 | } 51 | } 52 | 53 | task clean(type: Delete) { 54 | delete rootProject.buildDir 55 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliumujib/Bankable/c69f03c17575e4ae4bee1f851b32c7316fc7872b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat May 09 15:05:13 WAT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Bankable 2 | 3 | 4 | ### Introduction 5 | Android banking app uses Hover's USSD sdk to power fully offline banking and transaction analytics. The app aggregates banking operations of different app into one 6 | concise good looking application. 7 | 8 | ### Goals 9 | - Significantly improve the UX of USSD in terms of accessibility and aesthetics. 10 | - Achieve very lean APK size, currently at 6.3 MB with 2 bank integrations, can't go up much more than that even if we added more banks because we'd 11 | be adding only new contracts for the bank, and an SVG logo image for each bank. Size can be further optimized with code splitting and obfuscation. 12 | - Make intelligent use of Hover's SDK (Cut some corners though, for speed 🤣) 13 | - Aggregate USSD banking services in one place so the user doesn't have to remember them all. 14 | 15 | ### Used libraries 16 | **Hover SDK** - USSD calls
17 | **Dagger2** - Dagger2 was used for dependency injection.
18 | **Kotlin Flow** - Kotlin Flow was used for threading and data stream management.
19 | **AndroidKtx** - For cool extensions to Android classes.
20 | **Architecture Components** - For Lifecycle management etc.
21 | 22 | 23 | ### Possible Improvements 24 | We had a lot of fun building this. There are some improvements we intend to make. 25 | 26 | - Write tests.
27 | - Further explore the `Activity Result API` as a means of further abstracting Hover SDK logic from activities and fragments for a cleaner dev experience
28 | - Improve success/error parsing logic with parsers
29 | - Add more banks
30 | 31 | 32 | ### Team 33 | - [Abdul-Mujeeb Aliu](https://github.com/aliumujib). 34 | - [Quadri Anifowose](https://github.com/Quadriyanney). 35 | - [Olusesan Peter](https://sesan.design). 36 | 37 | ### Build Instructions 38 | - Clone repository.
39 | - Run with Android Studio 4.1 canary 8 and above.
40 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "OfflineBank" --------------------------------------------------------------------------------