Initial commit
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
76
app/build.gradle
Normal file
@@ -0,0 +1,76 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
defaultConfig {
|
||||
applicationId "com.example.minimoneybox"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
targetCompatibility = 1.8
|
||||
sourceCompatibility = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
|
||||
implementation 'com.airbnb.android:lottie:2.7.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.1.1'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
|
||||
//mock websever for testing retrofit responses
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.6.0")
|
||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||
|
||||
//mockito and livedata testing
|
||||
testImplementation 'org.mockito:mockito-inline:2.13.0'
|
||||
testImplementation 'androidx.arch.core:core-testing:2.1.0'
|
||||
|
||||
//Retrofit and GSON
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'
|
||||
|
||||
//Kotlin Coroutines
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
|
||||
|
||||
//Kodein Dependency Injection
|
||||
implementation "org.kodein.di:kodein-di-generic-jvm:6.2.1"
|
||||
implementation "org.kodein.di:kodein-di-framework-android-x:6.2.1"
|
||||
|
||||
//security for secured preferences
|
||||
implementation "androidx.security:security-crypto:1.0.0-rc01"
|
||||
|
||||
//keystore preferences wrapper
|
||||
implementation 'online.devliving:securedpreferencestore:0.7.4'
|
||||
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.example.minimoneybox
|
||||
|
||||
|
||||
import android.view.View
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.typeText
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import com.example.minimoneybox.ui.login.LoginActivity
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.TypeSafeMatcher
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LoginActivityTest {
|
||||
|
||||
val validEmail = "email_address@domain.com"
|
||||
val validPassword = "oLa95he!ui"
|
||||
val invalidEmail = "email_address"
|
||||
val invalidPassword = "oaa"
|
||||
val validName = "Ludacris"
|
||||
val invalidName = "Dj"
|
||||
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
var mActivityTestRule = ActivityTestRule(LoginActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun noEntriesSubmission_invalidLogin() {
|
||||
onView(withId(R.id.btn_sign_in)).perform(click())
|
||||
|
||||
onView(withId(R.id.til_password)).check(matches(hasTextInputLayoutErrorText(getString(R.string.password_error))))
|
||||
onView(withId(R.id.til_email)).check(matches(hasTextInputLayoutErrorText(getString(R.string.email_address_error))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validEmailNoPasswordNoName_invalidLogin() {
|
||||
onView(withId(R.id.et_email)).perform(typeText(validEmail)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.btn_sign_in)).perform(click())
|
||||
|
||||
onView(withId(R.id.til_password)).check(matches(hasTextInputLayoutErrorText(getString(R.string.password_error))))
|
||||
onView(withId(R.id.til_email)).check(matches(not(hasTextInputLayoutErrorText(getString(R.string.email_address_error)))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validPasswordNoEmailNoName_invalidLogin() {
|
||||
onView(withId(R.id.et_password)).perform(typeText(validPassword)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.btn_sign_in)).perform(click())
|
||||
|
||||
onView(withId(R.id.til_email)).check(matches(hasTextInputLayoutErrorText(getString(R.string.email_address_error))))
|
||||
onView(withId(R.id.til_password)).check(matches(not(hasTextInputLayoutErrorText(getString(R.string.password_error)))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validEmailAndPassword_noName_validLogin() {
|
||||
onView(withId(R.id.et_email)).perform(typeText(validEmail)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.et_password)).perform(typeText(validPassword)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.btn_sign_in)).perform(click())
|
||||
|
||||
onView(withId(R.id.til_email)).check(matches(not(hasTextInputLayoutErrorText(getString(R.string.email_address_error)))))
|
||||
onView(withId(R.id.til_password)).check(matches(not(hasTextInputLayoutErrorText(getString(R.string.password_error)))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validEmailAndPassword_validName_validLogin() {
|
||||
onView(withId(R.id.et_email)).perform(typeText(validEmail)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.et_password)).perform(typeText(validPassword)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.et_name)).perform(typeText(validName)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.btn_sign_in)).perform(click())
|
||||
|
||||
onView(withId(R.id.til_email)).check(matches(not(hasTextInputLayoutErrorText(getString(R.string.email_address_error)))))
|
||||
onView(withId(R.id.til_password)).check(matches(not(hasTextInputLayoutErrorText(getString(R.string.password_error)))))
|
||||
onView(withId(R.id.til_name)).check(matches(not(hasTextInputLayoutErrorText(getString(R.string.full_name_error)))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validEmailAndPassword_invalidName_validLogin() {
|
||||
onView(withId(R.id.et_email)).perform(typeText(validEmail)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.et_password)).perform(typeText(validPassword)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.et_name)).perform(typeText(invalidName)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.btn_sign_in)).perform(click())
|
||||
|
||||
onView(withId(R.id.til_email)).check(matches(not(hasTextInputLayoutErrorText(getString(R.string.email_address_error)))))
|
||||
onView(withId(R.id.til_password)).check(matches(not(hasTextInputLayoutErrorText(getString(R.string.password_error)))))
|
||||
onView(withId(R.id.til_name)).check(matches(hasTextInputLayoutErrorText(getString(R.string.full_name_error))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalidEmailValidPasswordNoName_invalidLogin() {
|
||||
onView(withId(R.id.et_email)).perform(typeText(invalidEmail)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.et_password)).perform(typeText(validPassword)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.btn_sign_in)).perform(click())
|
||||
|
||||
onView(withId(R.id.til_password)).check(matches(not(hasTextInputLayoutErrorText(getString(R.string.password_error)))))
|
||||
onView(withId(R.id.til_email)).check(matches(hasTextInputLayoutErrorText(getString(R.string.email_address_error))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validEmailInvalidPasswordNoName_invalidLogin() {
|
||||
onView(withId(R.id.et_email)).perform(typeText(validEmail)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.et_password)).perform(typeText(invalidPassword)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.btn_sign_in)).perform(click())
|
||||
|
||||
onView(withId(R.id.til_password)).check(matches(hasTextInputLayoutErrorText(getString(R.string.password_error))))
|
||||
onView(withId(R.id.til_email)).check(matches(not(hasTextInputLayoutErrorText(getString(R.string.email_address_error)))))
|
||||
}
|
||||
|
||||
private fun getString(id: Int): String = mActivityTestRule.activity.getString(id)
|
||||
|
||||
private fun hasTextInputLayoutErrorText(expectedErrorText: String): Matcher<View?>? {
|
||||
return object : TypeSafeMatcher<View?>() {
|
||||
|
||||
override fun matchesSafely(item: View?): Boolean {
|
||||
if (item !is TextInputLayout) {
|
||||
return false
|
||||
}
|
||||
val error = (item).error ?: return false
|
||||
val hint = error.toString()
|
||||
return expectedErrorText == hint
|
||||
}
|
||||
|
||||
override fun describeTo(description: org.hamcrest.Description?) {}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
33
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.example.minimoneybox">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<uses-sdk tools:overrideLibrary="androidx.security"/>
|
||||
|
||||
<application
|
||||
android:name=".application.MoneyBoxApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity android:name=".ui.user.AccountActivity"></activity>
|
||||
<activity
|
||||
android:name=".ui.login.LoginActivity"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
1
app/src/main/assets/pig.json
Normal file
@@ -0,0 +1,52 @@
|
||||
package com.example.minimoneybox.application
|
||||
|
||||
import android.app.Application
|
||||
import com.example.minimoneybox.data.network.LoginApi
|
||||
import com.example.minimoneybox.data.network.UserAccountApi
|
||||
import com.example.minimoneybox.data.network.interceptors.HeaderInterceptor
|
||||
import com.example.minimoneybox.data.network.interceptors.NetworkConnectionInterceptor
|
||||
import com.example.minimoneybox.data.prefs.KeystoreStorage
|
||||
import com.example.minimoneybox.data.repository.MoneyBoxRepositoryImpl
|
||||
import com.example.minimoneybox.ui.login.LoginViewModelFactory
|
||||
import com.example.minimoneybox.ui.user.UserAccountsViewModelFactory
|
||||
import com.google.gson.Gson
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.KodeinAware
|
||||
import org.kodein.di.android.x.androidXModule
|
||||
import org.kodein.di.generic.bind
|
||||
import org.kodein.di.generic.instance
|
||||
import org.kodein.di.generic.provider
|
||||
import org.kodein.di.generic.singleton
|
||||
|
||||
class MoneyBoxApplication: Application(), KodeinAware{
|
||||
|
||||
//Kodein aware to initialise the classes used for DI
|
||||
override val kodein = Kodein.lazy {
|
||||
import(androidXModule(this@MoneyBoxApplication))
|
||||
|
||||
//instance is context
|
||||
bind() from singleton { NetworkConnectionInterceptor(instance()) }
|
||||
bind() from singleton { HeaderInterceptor() }
|
||||
//instance is context
|
||||
bind() from singleton { KeystoreStorage(instance()) }
|
||||
//instances above 2 interceptors
|
||||
bind() from singleton { LoginApi(instance(), instance()) }
|
||||
bind() from singleton { UserAccountApi(instance(), instance()) }
|
||||
bind() from singleton { Gson() }
|
||||
//instances are context,
|
||||
bind() from singleton { MoneyBoxRepositoryImpl(instance(),instance(),instance(), instance()) }
|
||||
//Viewmodel created from context
|
||||
bind() from provider {
|
||||
LoginViewModelFactory(
|
||||
instance()
|
||||
)
|
||||
}
|
||||
bind() from provider {
|
||||
UserAccountsViewModelFactory(
|
||||
instance()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.example.minimoneybox.data.models
|
||||
|
||||
import com.google.gson.annotations.Expose
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
class Product {
|
||||
@SerializedName("Id")
|
||||
@Expose
|
||||
var id: Int? = null
|
||||
@SerializedName("Name")
|
||||
@Expose
|
||||
var name: String? = null
|
||||
@SerializedName("CategoryType")
|
||||
@Expose
|
||||
var categoryType: String? = null
|
||||
@SerializedName("Type")
|
||||
@Expose
|
||||
var type: String? = null
|
||||
@SerializedName("FriendlyName")
|
||||
@Expose
|
||||
var friendlyName: String? = null
|
||||
@SerializedName("CanWithdraw")
|
||||
@Expose
|
||||
var canWithdraw: Boolean? = null
|
||||
@SerializedName("ProductHexCode")
|
||||
@Expose
|
||||
var productHexCode: String? = null
|
||||
@SerializedName("AnnualLimit")
|
||||
@Expose
|
||||
var annualLimit: Double? = null
|
||||
@SerializedName("DepositLimit")
|
||||
@Expose
|
||||
var depositLimit: Double? = null
|
||||
@SerializedName("MinimumWeeklyDeposit")
|
||||
@Expose
|
||||
var minimumWeeklyDeposit: Double? = null
|
||||
@SerializedName("MaximumWeeklyDeposit")
|
||||
@Expose
|
||||
var maximumWeeklyDeposit: Double? = null
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.example.minimoneybox.data.models
|
||||
|
||||
import com.google.gson.annotations.Expose
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
class ProductsResponse {
|
||||
@SerializedName("Id")
|
||||
@Expose
|
||||
var id: Int? = null
|
||||
@SerializedName("PlanValue")
|
||||
@Expose
|
||||
var planValue: Double? = null
|
||||
@SerializedName("Moneybox")
|
||||
@Expose
|
||||
var moneybox: Double? = null
|
||||
@SerializedName("SubscriptionAmount")
|
||||
@Expose
|
||||
var subscriptionAmount: Double? = null
|
||||
@SerializedName("TotalFees")
|
||||
@Expose
|
||||
var totalFees: Double? = null
|
||||
@SerializedName("IsSelected")
|
||||
@Expose
|
||||
var isSelected: Boolean? = null
|
||||
@SerializedName("IsFavourite")
|
||||
@Expose
|
||||
var isFavourite: Boolean? = null
|
||||
@SerializedName("CollectionDayMessage")
|
||||
@Expose
|
||||
var collectionDayMessage: String? = null
|
||||
@SerializedName("Product")
|
||||
@Expose
|
||||
var product: Product? = null
|
||||
@SerializedName("state")
|
||||
@Expose
|
||||
var state: Int? = null
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.minimoneybox.data.models
|
||||
|
||||
import com.google.gson.annotations.Expose
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
class SessionObject {
|
||||
@SerializedName("BearerToken")
|
||||
@Expose
|
||||
var bearerToken: String? = null
|
||||
@SerializedName("ExternalSessionId")
|
||||
@Expose
|
||||
var externalSessionId: String? = null
|
||||
@SerializedName("SessionExternalId")
|
||||
@Expose
|
||||
var sessionExternalId: String? = null
|
||||
@SerializedName("ExpiryInSeconds")
|
||||
@Expose
|
||||
var expiryInSeconds: Int? = null
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.minimoneybox.data.models
|
||||
|
||||
data class UserLoginObject(
|
||||
val Email: String,
|
||||
val Password: String,
|
||||
val Idfa: String = "ANYTHING"
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.example.minimoneybox.data.network
|
||||
|
||||
import com.example.minimoneybox.data.network.interceptors.HeaderInterceptor
|
||||
import com.example.minimoneybox.data.network.interceptors.NetworkConnectionInterceptor
|
||||
import com.example.minimoneybox.data.network.response.LoginResponse
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface LoginApi {
|
||||
|
||||
@POST("users/login")
|
||||
suspend fun loginApiRequest(@Body body: HashMap<String, String>): Response<LoginResponse>
|
||||
|
||||
/**
|
||||
* Return [Retrofit] class with api to login
|
||||
* [NetworkConnectionInterceptor] to intercept when there is no network
|
||||
* [HeaderInterceptor] add custom headers to retrofit calls
|
||||
*/
|
||||
companion object{
|
||||
operator fun invoke(
|
||||
//injected @params
|
||||
networkConnectionInterceptor: NetworkConnectionInterceptor,
|
||||
customHeaderInterceptor: HeaderInterceptor
|
||||
) : LoginApi{
|
||||
|
||||
//okHttpClient with interceptors
|
||||
val okkHttpclient = OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(networkConnectionInterceptor)
|
||||
.addInterceptor(customHeaderInterceptor)
|
||||
.build()
|
||||
|
||||
//retrofit to be used in @Repository
|
||||
return Retrofit.Builder()
|
||||
.client(okkHttpclient)
|
||||
.baseUrl("https://api-test01.moneyboxapp.com/")
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(LoginApi::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.example.minimoneybox.data.network
|
||||
|
||||
import android.util.Log
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* This abstract class extract objects from [Response]
|
||||
*/
|
||||
abstract class SafeApiCall {
|
||||
|
||||
suspend fun <T : Any> responseUnwrap(
|
||||
call: suspend () -> Response<T>
|
||||
): T {
|
||||
val response = call.invoke()
|
||||
|
||||
if (response.isSuccessful) {
|
||||
//return the object within the response body
|
||||
return response.body()!!
|
||||
} else {
|
||||
//the response was unsuccessful
|
||||
//throw IOException error
|
||||
throw IOException(errorMessage(response))
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> errorMessage(errorResponse: Response<T>): String {
|
||||
val errorBody = errorResponse.errorBody()?.string()
|
||||
val errorCode = "Error Code: ${errorResponse.code()}"
|
||||
val errorMessageString = errorBody.getError()
|
||||
|
||||
//build a log message to log in console
|
||||
val log = if (errorMessageString.isNullOrEmpty()){
|
||||
errorCode
|
||||
}else{
|
||||
StringBuilder()
|
||||
.append(errorCode)
|
||||
.append("\n")
|
||||
.append(errorMessageString)
|
||||
.toString()
|
||||
}
|
||||
Log.e("Api Response Error", log)
|
||||
|
||||
//return error message
|
||||
//if null return error code
|
||||
return errorMessageString ?: errorCode
|
||||
}
|
||||
|
||||
private fun String?.getError(): String? {
|
||||
this?.let {
|
||||
try {
|
||||
//convert response to JSON
|
||||
//extract ["Message"] from error body
|
||||
return JSONObject(it).getString("Message")
|
||||
} catch (e: JSONException) {
|
||||
Log.e(SafeApiCall::class.java.simpleName, e.message)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.example.minimoneybox.data.network
|
||||
|
||||
import com.example.minimoneybox.data.network.interceptors.HeaderInterceptor
|
||||
import com.example.minimoneybox.data.network.interceptors.NetworkConnectionInterceptor
|
||||
import com.example.minimoneybox.data.network.response.PaymentResponse
|
||||
import com.example.minimoneybox.data.network.response.ProductApiResponse
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface UserAccountApi {
|
||||
|
||||
@GET("investorproducts")
|
||||
suspend fun getProductsFromApi(
|
||||
@Header("Authorization") authHeader: String
|
||||
): Response<ProductApiResponse>
|
||||
|
||||
@POST("oneoffpayments")
|
||||
suspend fun oneOffPaymentsFromApi(
|
||||
@Body body: HashMap<String, Int>,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Response<PaymentResponse>
|
||||
|
||||
/**
|
||||
* Return [Retrofit] class with api to User accounts
|
||||
* [NetworkConnectionInterceptor] to intercept when there is no network
|
||||
* [HeaderInterceptor] add custom headers to retrofit calls
|
||||
*/
|
||||
companion object {
|
||||
operator fun invoke(
|
||||
//injected @params
|
||||
networkConnectionInterceptor: NetworkConnectionInterceptor,
|
||||
customHeaderInterceptor: HeaderInterceptor
|
||||
): UserAccountApi {
|
||||
|
||||
//okHttpClient with interceptors
|
||||
val okkHttpclient = OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(networkConnectionInterceptor)
|
||||
.addInterceptor(customHeaderInterceptor)
|
||||
.build()
|
||||
|
||||
//retrofit to be used in @Repository
|
||||
return Retrofit.Builder()
|
||||
.client(okkHttpclient)
|
||||
.baseUrl("https://api-test01.moneyboxapp.com/")
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(UserAccountApi::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.minimoneybox.data.network.interceptors
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
|
||||
class HeaderInterceptor: Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val original = chain.request()
|
||||
|
||||
val request: Request = original.newBuilder()
|
||||
.header("AppId", "3a97b932a9d449c981b595")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("appVersion","7.8.0")
|
||||
.header("apiVersion", "3.0.0")
|
||||
.method(original.method(), original.body())
|
||||
.build()
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.minimoneybox.data.network.interceptors
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
|
||||
class NetworkConnectionInterceptor(
|
||||
context: Context
|
||||
) : Interceptor {
|
||||
|
||||
private val applicationContext = context.applicationContext
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !isInternetAvailable()) {
|
||||
throw IOException("Make sure you have an active data connection")
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun isInternetAvailable(): Boolean {
|
||||
var result = false
|
||||
val connectivityManager =
|
||||
applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
|
||||
connectivityManager?.let {
|
||||
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
|
||||
result = when {
|
||||
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.minimoneybox.data.network.response
|
||||
|
||||
import com.example.minimoneybox.data.models.SessionObject
|
||||
import com.google.gson.annotations.Expose
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class LoginResponse(
|
||||
@SerializedName("Session")
|
||||
@Expose
|
||||
val session: SessionObject? = null
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.minimoneybox.data.network.response
|
||||
|
||||
import com.google.gson.annotations.Expose
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
class PaymentResponse {
|
||||
@SerializedName("Moneybox")
|
||||
@Expose
|
||||
var moneybox: Double? = null
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.example.minimoneybox.data.network.response
|
||||
|
||||
import com.example.minimoneybox.data.models.ProductsResponse
|
||||
import com.google.gson.annotations.Expose
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
class ProductApiResponse {
|
||||
@SerializedName("MoneyboxEndOfTaxYear")
|
||||
@Expose
|
||||
var moneyboxEndOfTaxYear: String? = null
|
||||
@SerializedName("TotalPlanValue")
|
||||
@Expose
|
||||
var totalPlanValue: Double? = null
|
||||
@SerializedName("TotalEarnings")
|
||||
@Expose
|
||||
var totalEarnings: Double? = null
|
||||
@SerializedName("TotalContributionsNet")
|
||||
@Expose
|
||||
var totalContributionsNet: Double? = null
|
||||
@SerializedName("TotalEarningsAsPercentage")
|
||||
@Expose
|
||||
var totalEarningsAsPercentage: Double? = null
|
||||
@SerializedName("ProductResponses")
|
||||
@Expose
|
||||
var productsRespons: List<ProductsResponse>? = null
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.example.minimoneybox.data.prefs
|
||||
|
||||
import android.content.Context
|
||||
import devliving.online.securedpreferencestore.DefaultRecoveryHandler
|
||||
import devliving.online.securedpreferencestore.SecuredPreferenceStore
|
||||
|
||||
const val emailConstant = "EMAIL"
|
||||
const val passwordConstant = "PASSWORD"
|
||||
const val authToken = "AUTH"
|
||||
|
||||
/**
|
||||
* Secured preferences class storing values in keystore
|
||||
* inclusive of version down to [android.os.Build.VERSION_CODES.LOLLIPOP]
|
||||
*/
|
||||
class KeystoreStorage(context: Context) {
|
||||
|
||||
//lazy initialization
|
||||
private val prefStore: SecuredPreferenceStore by lazy {
|
||||
SecuredPreferenceStore.getSharedInstance()
|
||||
}
|
||||
|
||||
//init block to create instance of
|
||||
init {
|
||||
val storeFileName = "securedStore"
|
||||
val keyPrefix = "mba"
|
||||
val seedKey = "SecuredSeedData".toByteArray()
|
||||
SecuredPreferenceStore.init(
|
||||
context.applicationContext,
|
||||
storeFileName,
|
||||
keyPrefix,
|
||||
seedKey,
|
||||
DefaultRecoveryHandler()
|
||||
)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun saveCredentialsInPrefs(
|
||||
email: String,
|
||||
password: String
|
||||
) {
|
||||
val editor = prefStore.edit()
|
||||
editor.putString(emailConstant, email)
|
||||
editor.putString(passwordConstant, password)
|
||||
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun loadCredentialsFromPrefs(): Pair<String, String> {
|
||||
val email = prefStore.getString(emailConstant, "")
|
||||
val password = prefStore.getString(passwordConstant, "")
|
||||
|
||||
return Pair(email, password)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun saveTokenInPrefs(
|
||||
token: String
|
||||
) {
|
||||
val editor = prefStore.edit()
|
||||
editor.putString(authToken, token)
|
||||
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun loadTokenFromPrefs(): String? {
|
||||
return prefStore.getString(authToken, "")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.example.minimoneybox.data.prefs
|
||||
|
||||
import android.content.Context
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKeys
|
||||
|
||||
|
||||
class SecurePrefs(context: Context){
|
||||
|
||||
val emailConstant = "EMAIL"
|
||||
val passwordConstant = "PASSWORD"
|
||||
val nameConstant = "NAME"
|
||||
val authToken = "AUTH"
|
||||
|
||||
private val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
||||
private val prefs = EncryptedSharedPreferences.create(
|
||||
"encrypted_shared_prefs",
|
||||
masterKeyAlias,
|
||||
context,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
|
||||
@Synchronized
|
||||
fun saveCredentialsInPrefs(
|
||||
email: String,
|
||||
password: String
|
||||
) {
|
||||
val editor = prefs.edit()
|
||||
editor.putString(emailConstant, email)
|
||||
editor.putString(passwordConstant, password)
|
||||
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun loadCredentialsFromPrefs(): Pair<String,String> {
|
||||
val email = prefs.getString(emailConstant, "")
|
||||
val password = prefs.getString(passwordConstant, "")
|
||||
|
||||
return Pair(email, password)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun saveTokenInPrefs(
|
||||
token: String
|
||||
) {
|
||||
val editor = prefs.edit()
|
||||
editor.putString(authToken, token)
|
||||
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun loadTokenFromPrefs(): String? {
|
||||
return prefs.getString(authToken, "")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.example.minimoneybox.data.repository
|
||||
|
||||
import com.example.minimoneybox.data.network.interceptors.HeaderInterceptor
|
||||
import com.example.minimoneybox.data.network.interceptors.NetworkConnectionInterceptor
|
||||
import com.example.minimoneybox.data.network.response.LoginResponse
|
||||
import com.example.minimoneybox.data.network.response.PaymentResponse
|
||||
import com.example.minimoneybox.data.network.response.ProductApiResponse
|
||||
import retrofit2.Retrofit
|
||||
|
||||
/**
|
||||
* Repository interface to implement repository methods
|
||||
*
|
||||
*/
|
||||
interface MoneyBoxRepository {
|
||||
|
||||
suspend fun loginUser(email: String, password: String): LoginResponse?
|
||||
|
||||
fun saveCredentials(email: String, password: String)
|
||||
|
||||
fun loadCredentials(): Pair<String, String>
|
||||
|
||||
fun saveAuthToken(token: String)
|
||||
|
||||
fun loadAuthToken(): String?
|
||||
|
||||
suspend fun getProducts(authCode: String): ProductApiResponse?
|
||||
|
||||
suspend fun oneOffPayment(produceId: Int, authCode: String): PaymentResponse?
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.example.minimoneybox.data.repository
|
||||
|
||||
import com.example.minimoneybox.data.network.LoginApi
|
||||
import com.example.minimoneybox.data.network.UserAccountApi
|
||||
import com.example.minimoneybox.data.network.response.LoginResponse
|
||||
import com.example.minimoneybox.data.network.response.PaymentResponse
|
||||
import com.example.minimoneybox.data.network.response.ProductApiResponse
|
||||
import com.example.minimoneybox.data.prefs.KeystoreStorage
|
||||
import com.example.minimoneybox.data.network.SafeApiCall
|
||||
import com.example.minimoneybox.data.network.interceptors.HeaderInterceptor
|
||||
import com.example.minimoneybox.data.network.interceptors.NetworkConnectionInterceptor
|
||||
import com.google.gson.Gson
|
||||
import retrofit2.Retrofit
|
||||
|
||||
/**
|
||||
* [MoneyBoxRepository] implementations in this Repository
|
||||
*
|
||||
*/
|
||||
open class MoneyBoxRepositoryImpl(
|
||||
private val prefs: KeystoreStorage,
|
||||
private val loginApi: LoginApi,
|
||||
private val userAccountApi: UserAccountApi,
|
||||
private val gson: Gson
|
||||
) : MoneyBoxRepository, SafeApiCall() {
|
||||
|
||||
override suspend fun loginUser(
|
||||
email: String,
|
||||
password: String
|
||||
): LoginResponse{
|
||||
val hash = HashMap<String, String>()
|
||||
hash["Email"] = email
|
||||
hash["Password"] = password
|
||||
hash["Idfa"] = "ANYTHING"
|
||||
|
||||
return responseUnwrap{
|
||||
loginApi.loginApiRequest(hash)
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveCredentials(email: String, password: String) {
|
||||
prefs.saveCredentialsInPrefs(email, password)
|
||||
}
|
||||
|
||||
override fun loadCredentials(): Pair<String, String> {
|
||||
return prefs.loadCredentialsFromPrefs()
|
||||
}
|
||||
|
||||
override fun saveAuthToken(token: String) {
|
||||
prefs.saveTokenInPrefs(token)
|
||||
}
|
||||
|
||||
override fun loadAuthToken(): String? {
|
||||
return prefs.loadTokenFromPrefs()
|
||||
}
|
||||
|
||||
override suspend fun getProducts(authCode: String): ProductApiResponse {
|
||||
return responseUnwrap{
|
||||
userAccountApi.getProductsFromApi("Bearer $authCode")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun oneOffPayment(
|
||||
produceId: Int, authCode: String
|
||||
): PaymentResponse? {
|
||||
val hash = HashMap<String, Int>()
|
||||
hash["Amount"] = 20
|
||||
hash["InvestorProductId"] = produceId
|
||||
|
||||
return responseUnwrap {
|
||||
userAccountApi.oneOffPaymentsFromApi(hash,"Bearer $authCode")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.example.minimoneybox.ui.login
|
||||
|
||||
import android.animation.Animator
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.example.minimoneybox.R
|
||||
import com.example.minimoneybox.ui.user.AccountActivity
|
||||
import com.example.minimoneybox.utils.displayToast
|
||||
import com.example.minimoneybox.utils.hide
|
||||
import com.example.minimoneybox.utils.show
|
||||
import kotlinx.android.synthetic.main.activity_login.*
|
||||
import org.kodein.di.KodeinAware
|
||||
import org.kodein.di.android.kodein
|
||||
import org.kodein.di.generic.instance
|
||||
import java.util.regex.Pattern
|
||||
import android.util.Patterns.EMAIL_ADDRESS
|
||||
|
||||
/**
|
||||
* A login screen that offers login via email/password.
|
||||
*/
|
||||
class LoginActivity : AppCompatActivity(), KodeinAware {
|
||||
|
||||
//retrieve the viewmodel factory from the kodein dependency injection
|
||||
override val kodein by kodein()
|
||||
private val factory : LoginViewModelFactory by instance()
|
||||
|
||||
lateinit var viewModel: LoginViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_login)
|
||||
//views replaced with kotlin synthetic views
|
||||
//no need to use findViewById()
|
||||
setupViews()
|
||||
setupViewModel()
|
||||
setupLiveData()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setupAnimation()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
btn_sign_in.setOnClickListener {
|
||||
if (allFieldsValid()) {
|
||||
progress_circular.show()
|
||||
startLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLogin(){
|
||||
viewModel.attemptLogin(
|
||||
et_email.text.toString(),
|
||||
et_password.text.toString(),
|
||||
et_name.text.toString()
|
||||
)
|
||||
}
|
||||
|
||||
private fun allFieldsValid(): Boolean {
|
||||
var validity = true
|
||||
//Using Android Email address matcher
|
||||
if (!EMAIL_ADDRESS.matcher(et_email.text.toString()).matches()) {
|
||||
til_email.error = getString(R.string.email_address_error)
|
||||
validity = false
|
||||
}
|
||||
|
||||
if (!Pattern.matches(PASSWORD_REGEX, et_password.text.toString())) {
|
||||
til_password.error = getString(R.string.password_error)
|
||||
validity = false
|
||||
}
|
||||
|
||||
if (et_name.text.isNotEmpty() &&
|
||||
!Pattern.matches(NAME_REGEX, et_name.text.toString())
|
||||
) {
|
||||
til_name.error = getString(R.string.full_name_error)
|
||||
validity = false
|
||||
}
|
||||
|
||||
return validity
|
||||
}
|
||||
|
||||
private fun setupAnimation() {
|
||||
//animation completion listener attached
|
||||
//once the animation has run from frame first to frame last
|
||||
//then set new frame min and max then play animation again
|
||||
animation_view.apply {
|
||||
setAnimation("pig.json")
|
||||
setMinAndMaxFrame(firstAnim.first, firstAnim.second)
|
||||
addAnimatorListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
setMinAndMaxFrame(secondAnim.first, secondAnim.second)
|
||||
playAnimation()
|
||||
}
|
||||
override fun onAnimationRepeat(animation: Animator?) {}
|
||||
override fun onAnimationCancel(animation: Animator?) {}
|
||||
override fun onAnimationStart(animation: Animator?) {}
|
||||
})
|
||||
|
||||
playAnimation()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun setupViewModel(){
|
||||
viewModel = ViewModelProviders.of(this, factory).get(LoginViewModel::class.java)
|
||||
}
|
||||
|
||||
private fun setupLiveData(){
|
||||
viewModel.operationFailed.observe(this, Observer {
|
||||
progress_circular.hide()
|
||||
if (it != null) {
|
||||
displayToast(it)
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.operationSuccess.observe(this, Observer {
|
||||
progress_circular.hide()
|
||||
if (it == true){ startNewActivity() }
|
||||
})
|
||||
}
|
||||
|
||||
private fun startNewActivity(){
|
||||
Intent(this, AccountActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
putExtra("NAME_USER", viewModel.name);
|
||||
startActivity(this)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val EMAIL_REGEX = "[^@]+@[^.]+\\..+"
|
||||
val NAME_REGEX = "[a-zA-Z]{6,30}"
|
||||
val PASSWORD_REGEX = "^(?=.*[0-9])(?=.*[A-Z]).{10,50}$"
|
||||
val firstAnim = 0 to 109
|
||||
val secondAnim = 131 to 158
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.example.minimoneybox.ui.login
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.example.minimoneybox.data.repository.MoneyBoxRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
|
||||
class LoginViewModel(
|
||||
val repository: MoneyBoxRepository
|
||||
) : ViewModel() {
|
||||
|
||||
//stored @Nullable value for users name
|
||||
var name: String? = null
|
||||
|
||||
//operation results livedata based on outcome of operation
|
||||
val operationSuccess = MutableLiveData<Boolean>()
|
||||
val operationFailed = MutableLiveData<String>()
|
||||
|
||||
fun attemptLogin(
|
||||
email: String,
|
||||
password: String,
|
||||
usersName: String?
|
||||
){
|
||||
//clear name before operation
|
||||
name = null
|
||||
//open a coroutine on the IO thread and run async operations
|
||||
//Network calls and secured shared preferences
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
//retrieve response from API call from login api network request
|
||||
val response = repository.loginUser(email, password)
|
||||
|
||||
//null safety check to ensure bearer token exists
|
||||
response?.session?.bearerToken?.let {
|
||||
//save data in secured prefs
|
||||
repository.saveAuthToken(it)
|
||||
repository.saveCredentials(email,password)
|
||||
|
||||
//update name in this viewmodel for later use
|
||||
name = usersName
|
||||
|
||||
operationSuccess.postValue(true)
|
||||
return@launch
|
||||
}
|
||||
}catch (exception: IOException){
|
||||
operationFailed.postValue(
|
||||
exception.message ?: "could not receive token")
|
||||
return@launch
|
||||
}
|
||||
operationFailed.postValue("Could not login user")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.example.minimoneybox.ui.login
|
||||
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.example.minimoneybox.data.repository.MoneyBoxRepositoryImpl
|
||||
|
||||
/**
|
||||
* Viewmodel factory for [UserAccountsViewModel]
|
||||
* @repository injected into MainViewModel
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class LoginViewModelFactory(
|
||||
private val repositoryImpl: MoneyBoxRepositoryImpl
|
||||
): ViewModelProvider.NewInstanceFactory(){
|
||||
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return LoginViewModel(
|
||||
repositoryImpl
|
||||
) as T
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.example.minimoneybox.ui.user
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.example.minimoneybox.R
|
||||
import com.example.minimoneybox.utils.displayToast
|
||||
import com.example.minimoneybox.utils.hide
|
||||
import com.example.minimoneybox.utils.setAnimation
|
||||
import com.example.minimoneybox.utils.show
|
||||
import kotlinx.android.synthetic.main.account_activity.*
|
||||
import org.kodein.di.KodeinAware
|
||||
import org.kodein.di.android.kodein
|
||||
import org.kodein.di.generic.instance
|
||||
/**
|
||||
* Activity holding the fragments for products.
|
||||
*/
|
||||
class AccountActivity : AppCompatActivity(), KodeinAware {
|
||||
|
||||
//retrieve the viewmodel factory from the kodein dependency injection
|
||||
override val kodein by kodein()
|
||||
private val factory : UserAccountsViewModelFactory by instance()
|
||||
|
||||
val accountFragmentManager: FragmentManager by lazy {
|
||||
supportFragmentManager
|
||||
}
|
||||
|
||||
//to be used by the fragments
|
||||
lateinit var viewModel: UserAccountsViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.account_activity)
|
||||
|
||||
viewModel = ViewModelProviders
|
||||
.of(this , factory).get(UserAccountsViewModel::class.java)
|
||||
intent.extras?.getString("NAME_USER").let {
|
||||
viewModel.usersName = it
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
accountFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, UserAccountsFragment())
|
||||
.setAnimation()
|
||||
.commit()
|
||||
}
|
||||
|
||||
fetchData()
|
||||
setupOperationObservers()
|
||||
}
|
||||
|
||||
//override back button
|
||||
//conditional back pressing
|
||||
override fun onBackPressed() {
|
||||
if(accountFragmentManager.backStackEntryCount > 0){
|
||||
accountFragmentManager.popBackStack()
|
||||
}else{
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchData(){
|
||||
progress_circular_account.show()
|
||||
viewModel.getInvestorProducts()
|
||||
}
|
||||
|
||||
private fun setupOperationObservers(){
|
||||
viewModel.operationSuccess.observe(this, Observer {
|
||||
progress_circular_account.hide()
|
||||
})
|
||||
viewModel.operationFailed.observe(this, Observer {
|
||||
displayToast(it)
|
||||
progress_circular_account.hide()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.example.minimoneybox.ui.user
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import com.example.minimoneybox.R
|
||||
import com.example.minimoneybox.data.models.ProductsResponse
|
||||
import com.example.minimoneybox.utils.show
|
||||
import com.example.minimoneybox.utils.toCurrency
|
||||
import kotlinx.android.synthetic.main.account_activity.*
|
||||
import kotlinx.android.synthetic.main.investment_fragment.*
|
||||
import kotlinx.android.synthetic.main.investment_fragment.view.*
|
||||
|
||||
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
|
||||
private const val ARG_PARAM1 = "param1"
|
||||
|
||||
/**
|
||||
* A Fragment showing product selected in [UserAccountsFragment].
|
||||
*/
|
||||
class CurrentInvestmentFragment : Fragment() {
|
||||
private var currentItemPosition: Int? = null
|
||||
|
||||
private val accountActivity: AccountActivity by lazy {
|
||||
activity as AccountActivity
|
||||
}
|
||||
|
||||
//grab viewmodel from the activity hosting fragment
|
||||
private val viewModel by lazy {
|
||||
accountActivity.viewModel
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
currentItemPosition = it.getInt(ARG_PARAM1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(
|
||||
R.layout.investment_fragment, container,
|
||||
false
|
||||
).apply {
|
||||
//setup Observers after view instantiated
|
||||
setUpObserver()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpObserver(){
|
||||
viewModel.plans.observe(this, Observer { list ->
|
||||
//get list of products
|
||||
currentItemPosition?.let {
|
||||
populateViews(list[it])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//setting up views via kotlin synthetics
|
||||
//assign text and button on click
|
||||
private fun populateViews(currentProducts: ProductsResponse) {
|
||||
account_name_tv.text = currentProducts.product?.name
|
||||
friendly_account_tv.text = currentProducts.product?.friendlyName
|
||||
plan_val_tv.text = currentProducts.planValue?.toCurrency()
|
||||
moneybox_val_tv.text = currentProducts.moneybox?.toCurrency()
|
||||
btn_add_20.setOnClickListener {
|
||||
currentProducts.id?.let { id -> addOneOffPayment(id) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun addOneOffPayment(id: Int) {
|
||||
currentItemPosition?.let {
|
||||
accountActivity.progress_circular_account.show()
|
||||
viewModel.oneOffPayment(id)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* fragment Instance using the provided parameters.
|
||||
*
|
||||
* @param param1 Parameter 1.
|
||||
* @return A new instance of fragment CurrentInvestmentFragment.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newInstance(param1: Int) =
|
||||
CurrentInvestmentFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(ARG_PARAM1, param1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.example.minimoneybox.ui.user
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.example.minimoneybox.R
|
||||
import com.example.minimoneybox.ui.user.recyclerview.ProductsAdapter
|
||||
import com.example.minimoneybox.ui.user.recyclerview.RecyclerClickListener
|
||||
import com.example.minimoneybox.utils.setAnimation
|
||||
import com.example.minimoneybox.utils.toCurrency
|
||||
import kotlinx.android.synthetic.main.user_accounts_fragment.*
|
||||
|
||||
/**
|
||||
* A Fragment showing the first overview of products list.
|
||||
*/
|
||||
class UserAccountsFragment : Fragment(),
|
||||
RecyclerClickListener {
|
||||
|
||||
private val viewModel by lazy {
|
||||
(activity as AccountActivity).viewModel
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return inflater.inflate(
|
||||
R.layout.user_accounts_fragment, container, false
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
setupDataObservers()
|
||||
setupName()
|
||||
setupRecyclerView()
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
products_recycler_view.apply {
|
||||
setHasFixedSize(false)
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupName() {
|
||||
val name = viewModel.usersName
|
||||
|
||||
user_name_tv.text = if (name.isNullOrEmpty()) {
|
||||
"Hello !"
|
||||
} else {
|
||||
"Hello $name !"
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDataObservers() {
|
||||
viewModel.totalPlanValue.observe(this, Observer { planValue ->
|
||||
total_plan_val_tv.text = getTotalString(planValue)
|
||||
})
|
||||
viewModel.plans.observe(this, Observer {
|
||||
ProductsAdapter(
|
||||
it, this).apply {
|
||||
products_recycler_view.adapter = this
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun getTotalString(planValue: Double?): CharSequence? {
|
||||
return StringBuilder()
|
||||
.append("Total Plan: ")
|
||||
.append(planValue?.toCurrency())
|
||||
.toString()
|
||||
}
|
||||
|
||||
override fun onItemSelected(position: Int) {
|
||||
(activity as AccountActivity).accountFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(
|
||||
R.id.container,
|
||||
CurrentInvestmentFragment.newInstance(position)
|
||||
)
|
||||
.setAnimation()
|
||||
.addToBackStack("CurrentInvestment")
|
||||
.commit()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.example.minimoneybox.ui.user
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.example.minimoneybox.data.models.ProductsResponse
|
||||
import com.example.minimoneybox.data.network.response.PaymentResponse
|
||||
import com.example.minimoneybox.data.network.response.ProductApiResponse
|
||||
import com.example.minimoneybox.data.repository.MoneyBoxRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
|
||||
class UserAccountsViewModel(
|
||||
val repository: MoneyBoxRepository
|
||||
) : ViewModel() {
|
||||
|
||||
var usersName: String? = null
|
||||
|
||||
val totalPlanValue = MutableLiveData<Double>()
|
||||
val plans = MutableLiveData<List<ProductsResponse>>()
|
||||
|
||||
//operation results livedata based on outcome of operation
|
||||
val operationSuccess = MutableLiveData<Boolean>()
|
||||
val operationFailed = MutableLiveData<String>()
|
||||
|
||||
fun getInvestorProducts(){
|
||||
//retrieve auth token from safe storage/keystore
|
||||
//or finish operation
|
||||
val token = repository.loadAuthToken()
|
||||
if (token.isNullOrEmpty()){
|
||||
operationFailed.postValue("Failed to retrieve token")
|
||||
return
|
||||
}
|
||||
|
||||
//open a coroutine on the IO thread and run async operations
|
||||
//Network calls and secured shared preferences
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
repository.getProducts(token)?.let {
|
||||
getProducts(it)
|
||||
return@launch
|
||||
}
|
||||
}catch (exception: IOException){
|
||||
val credential = repository.loadCredentials()
|
||||
val loginResponse =
|
||||
repository.loginUser(credential.first, credential.second)
|
||||
|
||||
val code = loginResponse?.session?.bearerToken?.let {
|
||||
repository.saveAuthToken(it)
|
||||
it
|
||||
}
|
||||
|
||||
//retrieve response from API call from login api network request
|
||||
val response = code?.let { repository.getProducts(it) }
|
||||
response?.let {
|
||||
getProducts(it)
|
||||
return@launch
|
||||
}
|
||||
}catch (exception: IOException){
|
||||
operationFailed.postValue(exception.message)
|
||||
return@launch
|
||||
}
|
||||
operationFailed.postValue("Could not retrieve products")
|
||||
}
|
||||
}
|
||||
|
||||
fun oneOffPayment(productId: Int){
|
||||
//retrieve auth token from safe storage/keystore
|
||||
//or finish operation
|
||||
val token = repository.loadAuthToken()
|
||||
if (token.isNullOrEmpty()){
|
||||
operationFailed.postValue("Failed to retrieve token")
|
||||
return
|
||||
}
|
||||
|
||||
//open a coroutine on the IO thread and run async operations
|
||||
//Network calls and secured shared preferences
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
//retrieve response from API call from for one off repayment
|
||||
repository.oneOffPayment(productId, token)?.let {
|
||||
oneOffPaymentResponse(it)
|
||||
return@launch
|
||||
}
|
||||
}catch (exception: IOException){
|
||||
val credential = repository.loadCredentials()
|
||||
val loginResponse =
|
||||
repository.loginUser(credential.first, credential.second)
|
||||
|
||||
val code = loginResponse?.session?.bearerToken?.let {
|
||||
repository.saveAuthToken(it)
|
||||
it
|
||||
}
|
||||
//retrieve response from API call from login api network request
|
||||
val response = code?.let {
|
||||
repository.oneOffPayment(productId, code) }
|
||||
response?.let {
|
||||
oneOffPaymentResponse(it)
|
||||
return@launch
|
||||
}
|
||||
}catch (exception: IOException){
|
||||
operationFailed.postValue(exception.message)
|
||||
return@launch
|
||||
}
|
||||
operationFailed.postValue("Could not retrieve products")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProducts(response: ProductApiResponse){
|
||||
response.let {
|
||||
//update livedata with relevent data
|
||||
totalPlanValue.postValue(it.totalPlanValue)
|
||||
plans.postValue(it.productsRespons)
|
||||
|
||||
operationSuccess.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun oneOffPaymentResponse(response: PaymentResponse){
|
||||
//null safety check to ensure bearer token exists
|
||||
response.moneybox?.let {
|
||||
getInvestorProducts()
|
||||
|
||||
operationSuccess.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.minimoneybox.ui.user
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.example.minimoneybox.data.repository.MoneyBoxRepositoryImpl
|
||||
|
||||
/**
|
||||
* Viewmodel factory for [UserAccountsViewModel]
|
||||
* @repository injected into MainViewModel
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class UserAccountsViewModelFactory(
|
||||
private val repositoryImpl: MoneyBoxRepositoryImpl
|
||||
): ViewModelProvider.NewInstanceFactory(){
|
||||
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return UserAccountsViewModel(
|
||||
repositoryImpl
|
||||
) as T
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.example.minimoneybox.ui.user.recyclerview
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.example.minimoneybox.R
|
||||
import com.example.minimoneybox.data.models.ProductsResponse
|
||||
import com.example.minimoneybox.utils.toCurrency
|
||||
import kotlinx.android.synthetic.main.products_list_item.view.*
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
|
||||
/**
|
||||
* [ProductsResponse] recycler view adapter
|
||||
*/
|
||||
class ProductsAdapter(
|
||||
private val products: List<ProductsResponse>,
|
||||
private val recyclerClickListener: RecyclerClickListener
|
||||
): Adapter<ProductsAdapter.ProductsViewHolder>() {
|
||||
|
||||
class ProductsViewHolder(view: View) :
|
||||
ViewHolder(view) {
|
||||
var name = view.name_tv
|
||||
var planValue = view.plan_val_tv
|
||||
var moneyBoxVal = view.moneybox_val_tv
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): ProductsViewHolder {
|
||||
val view = LayoutInflater.from(p0.context)
|
||||
.inflate(R.layout.products_list_item, p0, false)
|
||||
|
||||
return ProductsViewHolder(view)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = products.size
|
||||
|
||||
override fun onBindViewHolder(p0: ProductsViewHolder, p1: Int) {
|
||||
p0.name.text = products[p1].product?.friendlyName
|
||||
p0.planValue.text = products[p1].planValue?.toCurrency()
|
||||
p0.moneyBoxVal.text = products[p1].moneybox?.toCurrency()
|
||||
|
||||
p0.itemView.setOnClickListener {
|
||||
recyclerClickListener.onItemSelected(p1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.minimoneybox.ui.user.recyclerview
|
||||
|
||||
interface RecyclerClickListener {
|
||||
fun onItemSelected(position: Int)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.example.minimoneybox.utils
|
||||
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
|
||||
fun Double.toCurrency(): String{
|
||||
NumberFormat.getCurrencyInstance().apply {
|
||||
maximumFractionDigits = 2
|
||||
currency = Currency.getInstance("GBP")
|
||||
return format(this@toCurrency)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.example.minimoneybox.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
|
||||
fun View.show(){
|
||||
this.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun View.hide(){
|
||||
this.visibility = View.GONE
|
||||
}
|
||||
|
||||
fun Context.displayToast(message: String){
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
fun FragmentTransaction.setAnimation() = apply {
|
||||
setCustomAnimations(
|
||||
android.R.anim.fade_in,
|
||||
android.R.anim.fade_out,
|
||||
android.R.anim.fade_in,
|
||||
android.R.anim.fade_out
|
||||
)
|
||||
}
|
||||
34
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0"/>
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?android:attr/colorControlHighlight">
|
||||
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#000000" />
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="24dp" />
|
||||
<solid android:color="@color/colorAccent" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</ripple>
|
||||
73
app/src/main/res/drawable/background_edit_text.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<layer-list >
|
||||
<item
|
||||
android:bottom="1dp"
|
||||
android:left="-2dp"
|
||||
android:right="-2dp"
|
||||
android:top="-2dp">
|
||||
<shape android:shape="rectangle" >
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="?attr/colorControlActivated" />
|
||||
|
||||
<solid android:color="#00FFFFFF" />
|
||||
|
||||
<padding android:left="-2dp"
|
||||
android:right="-2dp"
|
||||
android:top="8dp"
|
||||
android:bottom="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
</item>
|
||||
<item android:state_focused="true">
|
||||
<layer-list >
|
||||
<item
|
||||
android:bottom="1dp"
|
||||
android:left="-2dp"
|
||||
android:right="-2dp"
|
||||
android:top="-2dp">
|
||||
<shape android:shape="rectangle" >
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/colorAccent" />
|
||||
|
||||
<solid android:color="#00FFFFFF" />
|
||||
|
||||
<padding android:left="-2dp"
|
||||
android:right="-2dp"
|
||||
android:top="8dp"
|
||||
android:bottom="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
</item>
|
||||
<item>
|
||||
<layer-list >
|
||||
<item
|
||||
android:bottom="1dp"
|
||||
android:left="-2dp"
|
||||
android:right="-2dp"
|
||||
android:top="-2dp">
|
||||
<shape android:shape="rectangle" >
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/light_grey" />
|
||||
|
||||
<solid android:color="#00FFFFFF" />
|
||||
|
||||
<padding android:left="-2dp"
|
||||
android:right="-2dp"
|
||||
android:top="8dp"
|
||||
android:bottom="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
</item>
|
||||
|
||||
</selector>
|
||||
74
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path android:fillColor="#008577"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
</vector>
|
||||
317
app/src/main/res/drawable/moneybox_logo.xml
Normal file
@@ -0,0 +1,317 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="164dp"
|
||||
android:height="36dp"
|
||||
android:viewportWidth="164"
|
||||
android:viewportHeight="36">
|
||||
|
||||
<path
|
||||
android:fillColor="#546270"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M163.440023,30.5804348 L163.317925,30.380087 L157.585227,21.9392609
|
||||
C158.003175,21.3190435 158.363205,20.7802174 158.6661,20.3231739
|
||||
C158.979953,19.8485217 159.265237,19.4235652 159.520781,19.0498696
|
||||
C159.776324,18.675 160.011126,18.3341739 160.224796,18.0246522
|
||||
C160.438858,17.7151304 160.660355,17.3942609 160.885765,17.0612609
|
||||
C161.111175,16.7286522 161.357717,16.3690435 161.625392,15.9820435
|
||||
C161.893067,15.5950435 162.206136,15.1403478 162.559123,14.6218696
|
||||
C162.636216,14.5177826 162.704309,14.4046957 162.761444,14.2853478
|
||||
C162.825623,14.1487826 162.8538,14.0556522 162.8538,13.9848261
|
||||
C162.8538,13.793087 162.744616,13.6498696 162.545035,13.5837391
|
||||
C162.38576,13.530913 162.179134,13.505087 161.928678,13.505087
|
||||
L160.109743,13.505087 C159.803326,13.505087 159.568915,13.566913
|
||||
159.393987,13.6925217 C159.233148,13.8075652 159.05235,13.9977391
|
||||
158.83281,14.2818261 C158.558091,14.6993478 158.287677,15.1196087
|
||||
158.020003,15.5422174 C157.751154,15.9636522 157.487393,16.3831304
|
||||
157.225979,16.799087 L155.614061,19.3245652 L152.410575,14.3311304
|
||||
C152.184382,14.0161304 151.995366,13.8075652 151.835309,13.6925217
|
||||
C151.659599,13.566913 151.413839,13.505087 151.083551,13.505087
|
||||
L149.263833,13.505087 C149.025117,13.505087 148.826318,13.5316957
|
||||
148.674088,13.5845217 C148.480376,13.6514348 148.374323,13.7938696
|
||||
148.374323,13.9848261 C148.374323,14.0666087 148.404065,14.166
|
||||
148.468636,14.2974783 C148.525771,14.4105652 148.598168,14.5314783
|
||||
148.683871,14.652 C149.029813,15.1642174 149.332708,15.6071739
|
||||
149.594121,15.9824348 C149.856709,16.3561304 150.10012,16.707913
|
||||
150.324748,17.0342609 C150.549767,17.3606087 150.767742,17.681087
|
||||
150.97515,17.994913 C151.18295,18.3118696 151.418927,18.6566087
|
||||
151.681514,19.0326522 C151.943319,19.4063478 152.232908,19.8332609
|
||||
152.553805,20.3149565 C152.86257,20.7774783 153.230035,21.3186522
|
||||
153.659723,21.9400435 C152.725993,23.3166522 151.796176,24.6772174
|
||||
150.869098,26.0248696 C149.924019,27.3975652 148.975026,28.7812174
|
||||
148.024077,30.1711304 C147.938765,30.2928261 147.866368,30.4129565
|
||||
147.80845,30.5268261 C147.74427,30.654 147.712963,30.7573043
|
||||
147.712963,30.8406522 C147.712963,30.9455217 147.754837,31.089913
|
||||
147.951288,31.1896957 C148.078864,31.2526957 148.240095,31.284 148.442416,31.284
|
||||
L150.529809,31.284 C150.792787,31.284 151.022502,31.2229565
|
||||
151.212301,31.1012609 C151.394664,30.983087 151.575462,30.7968261
|
||||
151.75352,30.5416957 L155.612887,24.5359565 L159.518433,30.6086087
|
||||
C159.692969,30.8191304 159.868288,30.9850435 160.040476,31.1004783
|
||||
C160.223231,31.2221739 160.444336,31.284 160.697923,31.284 L162.820536,31.284
|
||||
C163.007204,31.284 163.16413,31.2566087 163.28701,31.2010435
|
||||
C163.499897,31.107913 163.531987,30.9463043 163.531987,30.8578696
|
||||
C163.532378,30.7733478 163.502637,30.6821739 163.440023,30.5804348
|
||||
L163.440023,30.5804348 Z" />
|
||||
<path
|
||||
android:fillColor="#EB6A6C"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M142.416997,25.0301739 C141.621409,24.2354348 140.673199,23.598
|
||||
139.600152,23.1354783 C138.524757,22.6721739 137.364051,22.4366087
|
||||
136.152471,22.4366087 C134.929151,22.4366087 133.769228,22.6725652
|
||||
132.704399,23.1354783 C131.641528,23.5987826 130.699579,24.2358261
|
||||
129.905164,25.0301739 C129.109967,25.826087 128.475218,26.7730435
|
||||
128.018919,27.8471739 C127.562229,28.9185652 127.330166,30.0752609
|
||||
127.328992,31.2847826 L130.345419,31.2847826 L142.009615,31.2847826
|
||||
L145.009606,31.2847826 C145.008041,30.0752609 144.774021,28.9185652
|
||||
144.311852,27.8463913 C143.848509,26.7706957 143.210238,25.8245217
|
||||
142.416997,25.0301739 L142.416997,25.0301739 Z" />
|
||||
<path
|
||||
android:fillColor="#5FBEBA"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M 136.169299 0.26608695 C 141.051665867 0.26608695 145.00960623 4.22369510386 145.00960623 9.10565217 C 145.00960623 13.9876092361 141.051665867 17.94521739 136.169299 17.94521739 C 131.286932133 17.94521739 127.32899177 13.9876092361 127.32899177 9.10565217 C 127.32899177 4.22369510386 131.286932133 0.26608695 136.169299 0.26608695 Z" />
|
||||
<path
|
||||
android:fillColor="#546270"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M123.340092,15.9065217 C122.593029,15.0757826 121.701563,14.4101739
|
||||
120.69113,13.9288696 C119.677567,13.4463913 118.577518,13.2018261
|
||||
117.420334,13.2018261 C116.188404,13.2018261 115.095791,13.4694783
|
||||
114.174975,13.9965652 C113.386039,14.4469565 112.715678,15.0245217
|
||||
112.174459,15.7147826 L112.174459,10.1195217 C112.174459,9.80843478
|
||||
112.075451,9.558 111.879782,9.37526087 C111.685287,9.19682609
|
||||
111.401959,9.10565217 111.034102,9.10565217 L109.999798,9.10565217
|
||||
C109.658943,9.10565217 109.374833,9.19526087 109.156075,9.36782609
|
||||
C108.923621,9.54978261 108.807002,9.80334783 108.807002,10.119913
|
||||
L108.807002,30.234913 C108.807002,30.560087 108.907967,30.8191304
|
||||
109.109115,31.0069565 C109.307914,31.1920435 109.590068,31.2851739
|
||||
109.945794,31.2851739 L111.016101,31.2851739 C111.357738,31.2851739
|
||||
111.637153,31.1963478 111.846518,31.0206522 C112.06371,30.8398696
|
||||
112.174067,30.5749565 112.174067,30.234913 L112.174067,29.1858261
|
||||
C112.728201,29.8588696 113.418911,30.423913 114.231718,30.8664783
|
||||
C115.175232,31.3810435 116.248279,31.6420435 117.419943,31.6420435
|
||||
C118.578692,31.6420435 119.678741,31.4002174 120.690739,30.924
|
||||
C121.701171,30.4485652 122.592246,29.786087 123.3397,28.9549565
|
||||
C124.085589,28.1265652 124.679248,27.1416522 125.106588,26.0315217
|
||||
C125.533537,24.9174783 125.749163,23.7091304 125.749163,22.4397391
|
||||
C125.749163,21.1699565 125.533537,19.9588696 125.106588,18.8401304
|
||||
C124.679639,17.7229565 124.085198,16.736087 123.340092,15.9065217
|
||||
L123.340092,15.9065217 Z M121.94654,24.7691739 C121.656168,25.4938696
|
||||
121.264831,26.1332609 120.785051,26.672087 C120.306446,27.2085652
|
||||
119.744486,27.6421304 119.11365,27.9602609 C117.859806,28.5910435
|
||||
116.407553,28.5906522 115.146273,27.9594783 C114.510351,27.6417391
|
||||
113.941738,27.2077826 113.456871,26.6713043 C112.97083,26.1332609
|
||||
112.581058,25.4934783 112.29773,24.7699565 C112.013619,24.0468261
|
||||
111.87039,23.268913 111.87039,22.4577391 C111.87039,21.6465652
|
||||
112.013619,20.8694348 112.29773,20.145913 C112.580275,19.4223913
|
||||
112.97083,18.7802609 113.457653,18.2347826 C113.942129,17.6920435
|
||||
114.510351,17.2592609 115.145099,16.9477826 C115.775544,16.6374783
|
||||
116.445904,16.4801739 117.135049,16.4801739 C117.823411,16.4801739
|
||||
118.488684,16.6374783 119.115215,16.9477826 C119.743312,17.2584783
|
||||
120.30488,17.6920435 120.785051,18.2347826 C121.265613,18.7802609
|
||||
121.656168,19.4223913 121.94654,20.147087 C122.235347,20.871
|
||||
122.382098,21.648913 122.382098,22.4577391 C122.38249,23.2677391
|
||||
122.235738,24.0444783 121.94654,24.7691739 L121.94654,24.7691739 Z" />
|
||||
<path
|
||||
android:fillColor="#546270"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M104.579387,13.5054783 L103.116177,13.5054783 C102.855546,13.5054783
|
||||
102.633658,13.5571304 102.456774,13.6576957 C102.276758,13.7633478
|
||||
102.131572,13.9366957 102.022781,14.1836087 L97.0481022,26.8098261
|
||||
C96.682202,25.9231304 96.3014309,24.9867391 95.8877875,23.9583913
|
||||
C95.4537945,22.8756522 95.0201929,21.7886087 94.5869826,20.6941304
|
||||
C94.1529897,19.6000435 93.7209534,18.509087 93.2928305,17.4212609
|
||||
C92.8560982,16.3115217 92.4424548,15.2401304 92.0644231,14.2423043
|
||||
C91.9826336,14.0126087 91.8480136,13.8306522 91.6644765,13.7015217
|
||||
C91.479374,13.5708261 91.2574858,13.5058696 91.0038993,13.5058696
|
||||
L89.486685,13.5058696 C88.6977492,13.5058696 88.5799567,13.9519565
|
||||
88.5799567,14.2180435 C88.5799567,14.2751739 88.5889574,14.3694783
|
||||
88.606959,14.4970435 C88.6284825,14.6336087 88.676617,14.7928696
|
||||
88.7505797,14.9662174 L95.2941289,30.789 L93.7988295,34.5545217
|
||||
C93.745999,34.7110435 93.7197794,34.8507391 93.7197794,34.9728261
|
||||
C93.7197794,35.3136522 93.8872717,35.7206087 94.6812949,35.7206087
|
||||
L96.1081103,35.7206087 C96.6191967,35.7206087 96.9804009,35.4823043
|
||||
97.1827222,35.0107826 C98.3129041,32.3342609 98.9323908,30.789 99.696281,28.89
|
||||
C99.9745218,28.197 100.291505,27.4069565 100.696148,26.4075652
|
||||
C102.176576,22.746913 103.712575,19.0177826 105.260313,15.3258261
|
||||
C105.411369,14.9857826 105.493942,14.7697826 105.518596,14.6476957
|
||||
C105.545598,14.5248261 105.558121,14.4297391 105.558121,14.3596957
|
||||
C105.558121,14.1088696 105.479071,13.9018696 105.320971,13.7437826
|
||||
C105.163262,13.5856957 104.91398,13.5054783 104.579387,13.5054783
|
||||
L104.579387,13.5054783 Z" />
|
||||
<path
|
||||
android:fillColor="#546270"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M84.4192596,15.6357391 C83.6796323,14.8664348 82.8073417,14.2497391
|
||||
81.8250854,13.8040435 C80.8412637,13.3567826 79.7862186,13.1313913
|
||||
78.6900831,13.1313913 C77.5078534,13.1313913 76.3831503,13.379087
|
||||
75.3476721,13.8666522 C74.3125852,14.3538261 73.3984216,15.024913
|
||||
72.62827,15.8615217 C71.8592924,16.6969565 71.2433276,17.6896957
|
||||
70.7991599,18.8111739 C70.3549922,19.9353913 70.1287993,21.1503913
|
||||
70.1287993,22.4225217 C70.1287993,23.7056087 70.3549922,24.9233478
|
||||
70.7991599,26.0413043 C71.2441103,27.1584783 71.8592924,28.1484783
|
||||
72.62827,28.9835217 C73.3976389,29.8189565 74.3121939,30.4865217
|
||||
75.3484548,30.9686087 C76.386281,31.4503043 77.5105928,31.6952609
|
||||
78.6900831,31.6952609 C79.1694712,31.6952609 79.6825142,31.6467391
|
||||
80.2135588,31.5493043 C80.744212,31.4534348 81.2701692,31.3196087
|
||||
81.7749942,31.152913 C82.2876459,30.9842609 82.7799481,30.7772609
|
||||
83.2413347,30.5373913 C83.7038952,30.2967391 84.115582,30.0349565
|
||||
84.458002,29.763 C84.6117975,29.6604783 84.759723,29.5262609
|
||||
84.8986477,29.3611304 C85.0504865,29.1807391 85.1271886,28.9823478
|
||||
85.1271886,28.7718261 C85.1271886,28.6485652 85.067314,28.4904783
|
||||
84.9475648,28.2893478 L84.2134163,27.0563478 C84.1394535,26.9366087
|
||||
84.0310531,26.8462174 83.8901717,26.7875217 C83.615453,26.6716957
|
||||
83.306688,26.6658261 82.9122201,26.8516957 C82.7122468,26.9448261
|
||||
82.5400584,27.0496957 82.3870456,27.176087 C82.1725928,27.4022609
|
||||
81.9158756,27.5936087 81.6219814,27.7454348 C81.3190864,27.9035217
|
||||
80.9993639,28.035 80.6714233,28.1375217 C80.3430914,28.2423913
|
||||
80.0022367,28.3183043 79.6598167,28.3652609 C79.3197446,28.4114348
|
||||
78.9933694,28.4353043 78.6896918,28.4353043 C78.0455507,28.4353043
|
||||
77.4299773,28.3183043 76.8601904,28.0878261 C76.2880554,27.8569565
|
||||
75.7675769,27.5325652 75.3136258,27.1248261 C74.8573266,26.7143478
|
||||
74.4722508,26.2146522 74.1666165,25.6374783 C73.884071,25.1072609
|
||||
73.6856631,24.5175652 73.5737406,23.8824783 L84.3613417,23.8824783
|
||||
C84.6857603,23.8824783 84.9945253,23.8633043 85.2802015,23.826913
|
||||
C85.5772264,23.7893478 85.841379,23.7052174 86.0636585,23.5776522
|
||||
C86.2921994,23.4477391 86.4780846,23.2622609 86.615444,23.0278696
|
||||
C86.7520206,22.7926957 86.820896,22.4816087 86.820896,22.1004783
|
||||
L86.820896,21.6907826 C86.820896,20.5125652 86.6044865,19.3903043
|
||||
86.1771463,18.3545217 C85.7513714,17.321087 85.1596696,16.4073913
|
||||
84.4192596,15.6357391 L84.4192596,15.6357391 Z M73.7130567,20.9793913
|
||||
C73.8648955,20.3916522 74.0856097,19.8289565 74.3708945,19.305
|
||||
C74.6847469,18.7313478 75.060822,18.2238261 75.4889449,17.7945652
|
||||
C75.9147197,17.3688261 76.3984125,17.0272174 76.9290657,16.7795217
|
||||
C77.9801974,16.2884348 79.217997,16.2876522 80.2812601,16.7795217
|
||||
C80.8181748,17.028 81.3112596,17.3696087 81.749166,17.7973043
|
||||
C82.1905943,18.2265652 82.568626,18.7333043 82.8738691,19.3046087
|
||||
C83.1540665,19.8301304 83.3575618,20.3928261 83.4788764,20.9797826
|
||||
L73.7130567,20.9797826 L73.7130567,20.9793913 Z" />
|
||||
<path
|
||||
android:fillColor="#546270"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M64.6986044,15.3790435 C64.0642468,14.6966087 63.2991826,14.151913
|
||||
62.4233699,13.7582609 C61.547166,13.3653913 60.5762584,13.167 59.5388235,13.167
|
||||
C58.9901687,13.167 58.4559935,13.2499565 57.9523425,13.4146957
|
||||
C57.4510395,13.5778696 56.9704775,13.7950435 56.5239617,14.0599565
|
||||
C56.0770546,14.3252609 55.6645852,14.6320435 55.2967283,14.9693478
|
||||
C55.0239663,15.2186087 54.7750758,15.471 54.5504482,15.7222174
|
||||
L54.4518313,14.4520435 C54.4240463,14.1824348 54.3266034,13.9617391
|
||||
54.1610677,13.7966087 C53.9927927,13.6271739 53.7677739,13.541087
|
||||
53.4914898,13.541087 L52.1535079,13.541087 C51.8920947,13.541087
|
||||
51.6682498,13.626 51.487452,13.7926957 C51.3039149,13.962913
|
||||
51.2103853,14.1906522 51.2103853,14.4673043 L51.2103853,30.3597391
|
||||
C51.2103853,30.9654783 51.5485006,31.2855652 52.1887283,31.2855652
|
||||
L53.6523294,31.2855652 C53.9247001,31.2855652 54.148545,31.2065217
|
||||
54.3176026,31.0519565 C54.489791,30.8950435 54.5774505,30.6622174
|
||||
54.5774505,30.3593478 L54.5774505,20.5927826 C54.6114969,20.0332174
|
||||
54.764901,19.4975217 55.032967,19.0017391 C55.3029897,18.5020435
|
||||
55.6501058,18.0618261 56.0649232,17.6928261 C56.4785666,17.3257826
|
||||
56.95365,17.0303478 57.4772591,16.8166957 C58.5440443,16.3846957
|
||||
59.7247086,16.3972174 60.7218358,16.833913 C61.2337049,17.0581304
|
||||
61.6896127,17.366087 62.0805585,17.7507391 C62.4703304,18.135
|
||||
62.7818348,18.5908696 63.004897,19.1066087 C63.2275679,19.6207826
|
||||
63.3406643,20.1733043 63.3406643,20.745 L63.3406643,30.2713043
|
||||
C63.3406643,30.6027391 63.4024956,30.8453478 63.5292888,31.0155652
|
||||
C63.6635175,31.1947826 63.9112339,31.2855652 64.2669595,31.2855652
|
||||
L65.7289952,31.2855652 C66.0021486,31.2855652 66.2385162,31.1947826
|
||||
66.4306628,31.0159565 C66.6267227,30.8343913 66.7261224,30.5718261
|
||||
66.7261224,30.234913 L66.7261224,20.745 C66.7261224,19.6986522
|
||||
66.5461073,18.7023913 66.1892078,17.7871304 C65.8342649,16.8718696
|
||||
65.3325706,16.0614783 64.6986044,15.3790435 L64.6986044,15.3790435 Z" />
|
||||
<path
|
||||
android:fillColor="#546270"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M45.1470068,15.8212174 C44.3103279,14.9861739 43.3147661,14.3154783
|
||||
42.1865409,13.8294783 C41.0551851,13.343087 39.8361697,13.0957826
|
||||
38.5627585,13.0957826 C37.2772158,13.0957826 36.0578091,13.343087
|
||||
34.9389761,13.8294783 C33.8220997,14.3162609 32.832408,14.9861739
|
||||
31.996903,15.8212174 C31.1610068,16.6566522 30.4941682,17.652913
|
||||
30.0139975,18.7826087 C29.5330441,19.9123043 29.2884583,21.1304348
|
||||
29.2884583,22.4045217 C29.2884583,23.6911304 29.5326527,24.9127826
|
||||
30.0139975,26.0358261 C30.4937769,27.1588696 31.1613981,28.1508261
|
||||
31.996903,28.9878261 C32.8316253,29.8224783 33.8217084,30.4892609
|
||||
34.9401501,30.9693913 C36.0570264,31.4503043 37.2764331,31.6948696
|
||||
38.5627585,31.6948696 C39.8365611,31.6948696 41.0559678,31.4506957
|
||||
42.1857582,30.9693913 C43.3147661,30.4896522 44.3111106,29.8236522
|
||||
45.1470068,28.9878261 C45.9813377,28.1535652 46.6516983,27.1608261
|
||||
47.1385218,26.0366087 C47.6253453,24.9127826 47.872279,23.6911304
|
||||
47.872279,22.4045217 C47.872279,21.1304348 47.6253453,19.9115217
|
||||
47.1385218,18.7818261 C46.6509156,17.6509565 45.980555,16.6546957
|
||||
45.1470068,15.8212174 L45.1470068,15.8212174 Z M44.2312778,24.7734783
|
||||
C43.9076419,25.5146087 43.4591695,26.172 42.8972093,26.7284348
|
||||
C42.3352491,27.284087 41.6746719,27.7309565 40.9326965,28.0549565
|
||||
C40.1926779,28.3789565 39.3959154,28.5433043 38.5627585,28.5433043
|
||||
C37.7303843,28.5433043 36.9359698,28.3789565 36.2022125,28.0549565
|
||||
C35.4661073,27.7313478 34.8114002,27.284087 34.2560927,26.7284348
|
||||
C33.6996112,26.172 33.2566175,25.515 32.9388517,24.7742609
|
||||
C32.6210859,24.0362609 32.4598549,23.2391739 32.4598549,22.4045217
|
||||
C32.4598549,21.5702609 32.6210859,20.7723913 32.9388517,20.0336087
|
||||
C33.2566175,19.2940435 33.7003939,18.6331304 34.2560927,18.0700435
|
||||
C34.8114002,17.5096957 35.4661073,17.0612609 36.2022125,16.7356957
|
||||
C36.9359698,16.4128696 37.7303843,16.2481304 38.5627585,16.2481304
|
||||
C39.3951327,16.2481304 40.1922866,16.4128696 40.9326965,16.7356957
|
||||
C41.6746719,17.0612609 42.3356404,17.510087 42.8972093,18.0700435
|
||||
C43.4583868,18.6327391 43.9080332,19.2940435 44.2312778,20.0343913
|
||||
C44.5556963,20.7739565 44.7200579,21.5718261 44.7200579,22.4045217
|
||||
C44.7196666,23.2376087 44.5556963,24.0346957 44.2312778,24.7734783
|
||||
L44.2312778,24.7734783 Z" />
|
||||
<path
|
||||
android:fillColor="#546270"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M24.0699768,15.4275652 C23.4716223,14.738087 22.7421697,14.184
|
||||
21.9023601,13.7782174 C21.0602025,13.3728261 20.1127751,13.1666087
|
||||
19.0859063,13.1666087 C18.4073276,13.1666087 17.7800141,13.2656087
|
||||
17.2192279,13.4604783 C16.6603984,13.6545652 16.1442246,13.9253478
|
||||
15.6836208,14.2646087 C15.2234082,14.6034783 14.8089821,15.0088696
|
||||
14.4520826,15.4698261 C14.1495789,15.858 13.8729036,16.2786522
|
||||
13.6267525,16.7212174 C13.3942982,16.3095652 13.1094047,15.9116087
|
||||
12.7779421,15.5336087 C12.3854309,15.0867391 11.9291317,14.688
|
||||
11.421176,14.3503043 C10.9132203,14.0118261 10.3426507,13.7383043
|
||||
9.72746865,13.539913 C9.10993855,13.3403478 8.43488189,13.2382174
|
||||
7.72147419,13.2382174 C7.15090456,13.2382174 6.6316001,13.3196087
|
||||
6.17686628,13.4776957 C5.72721984,13.6365652 5.31396777,13.845913
|
||||
4.94806749,14.103 C4.58529791,14.3561739 4.25266129,14.6516087
|
||||
3.95994107,14.9803043 C3.7603591,15.2056957 3.56899522,15.4365652
|
||||
3.39054545,15.6693913 L3.29505918,14.4532174 C3.26805691,14.1855652
|
||||
3.17765802,13.9656522 3.02660186,13.802087 C2.86850164,13.6295217
|
||||
2.63604734,13.5422609 2.33432637,13.5422609 L0.996344498,13.5422609
|
||||
C0.722017124,13.5422609 0.49386754,13.6271739 0.319331151,13.7962174
|
||||
C0.142838076,13.9656522 0.0532218585,14.1918261 0.0532218585,14.468087
|
||||
L0.0532218585,30.3605217 C0.0532218585,30.9654783 0.385858474,31.2855652
|
||||
1.01473735,31.2855652 L2.49555729,31.2855652 C2.76831932,31.2855652
|
||||
2.99294686,31.2073043 3.16122186,31.0519565 C3.33380156,30.8958261
|
||||
3.42067842,30.6622174 3.42067842,30.3601304 L3.42067842,20.6021739
|
||||
C3.42067842,20.0426087 3.52594812,19.5045652 3.73218283,19.0060435
|
||||
C3.93724352,18.5086957 4.22526769,18.0673043 4.58608058,17.6943913
|
||||
C4.94532813,17.3234348 5.37462503,17.0276087 5.86066583,16.8155217
|
||||
C6.34670662,16.605 6.8714898,16.4977826 7.4185792,16.4977826
|
||||
C7.97858272,16.4977826 8.50258323,16.6073478 8.97492722,16.8245217
|
||||
C9.44844523,17.040913 9.86639335,17.339087 10.2154661,17.7108261
|
||||
C10.5641476,18.0813913 10.8392576,18.5223913 11.0345349,19.0216957
|
||||
C11.2290295,19.5198261 11.3280378,20.0633478 11.3280378,20.6385652
|
||||
L11.3280378,30.2701304 C11.3280378,30.603913 11.3933911,30.848087
|
||||
11.5276197,31.017913 C11.6681098,31.1951739 11.9123042,31.2847826
|
||||
12.2535502,31.2847826 L13.6803657,31.2847826 C14.3444649,31.2847826
|
||||
14.6947117,30.9220435 14.6947117,30.2345217 L14.6947117,20.6921739
|
||||
C14.6947117,20.0966087 14.8136782,19.5378261 15.0473065,19.0291304
|
||||
C15.2825001,18.5184783 15.602614,18.072 15.9978645,17.6998696
|
||||
C16.3931151,17.3277391 16.852545,17.0303478 17.36324,16.817087
|
||||
C17.875109,16.6053913 18.412415,16.4985652 18.9606784,16.4985652
|
||||
C19.5187253,16.4985652 20.0266809,16.6073478 20.468892,16.8233478
|
||||
C20.911103,17.0389565 21.2922654,17.3347826 21.6002478,17.7049565
|
||||
C21.9086215,18.0763043 22.1489025,18.516913 22.3144382,19.0158261
|
||||
C22.4811478,19.515913 22.5656767,20.0629565 22.5656767,20.6389565
|
||||
L22.5656767,30.2705217 C22.5656767,30.7338261 22.7546925,31.2851739
|
||||
23.6516374,31.2851739 L24.9187872,31.2851739 C25.2154208,31.2851739
|
||||
25.4631372,31.1951739 25.6556751,31.0159565 C25.8517351,30.8343913
|
||||
25.9507434,30.5718261 25.9507434,30.234913 L25.9507434,20.6921739
|
||||
C25.9507434,19.6943478 25.7859904,18.7297826 25.4611806,17.8270435
|
||||
C25.1351967,16.9223478 24.6675487,16.1146957 24.0699768,15.4275652
|
||||
L24.0699768,15.4275652 Z" />
|
||||
</vector>
|
||||
17
app/src/main/res/layout/account_activity.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.user.AccountActivity" >
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_circular_account"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:elevation="12dp"
|
||||
android:layout_gravity="center"/>
|
||||
</FrameLayout>
|
||||
121
app/src/main/res/layout/activity_login.xml
Normal file
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.login.LoginActivity">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/img_logo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:src="@drawable/moneybox_logo"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/til_email"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginLeft="18dp"
|
||||
android:layout_marginRight="18dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/img_logo">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_email"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/prompt_email"
|
||||
android:inputType="textEmailAddress"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/til_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginLeft="18dp"
|
||||
android:layout_marginRight="18dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/til_email">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/prompt_password"
|
||||
android:imeActionLabel="@string/action_sign_in_short"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/til_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginLeft="18dp"
|
||||
android:layout_marginRight="18dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/til_password">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/prompt_full_name"
|
||||
android:inputType="textPersonName"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/animation_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="fitEnd"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/btn_sign_in"
|
||||
tools:layout_height="40dp"
|
||||
tools:layout_width="40dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_sign_in"
|
||||
style="@style/Widget.Button.Colored.Rounded"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_sign_in"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/til_name"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_circular"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
76
app/src/main/res/layout/investment_fragment.xml
Normal file
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="@color/light_grey"
|
||||
android:id="@+id/useraccounts"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.user.AccountActivity">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_name_tv"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_margin="12dp"
|
||||
android:textColor="@color/dark_text"
|
||||
android:textStyle="bold"
|
||||
android:textSize="24sp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hello" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/friendly_account_tv"
|
||||
app:layout_constraintTop_toBottomOf="@id/account_name_tv"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:textColor="@color/dark_text"
|
||||
android:layout_margin="12dp"
|
||||
android:textStyle="bold"
|
||||
android:textSize="24sp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Total Plan: £7000" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/plan_val_tv"
|
||||
app:layout_constraintTop_toBottomOf="@id/friendly_account_tv"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_margin="12dp"
|
||||
android:textColor="@color/dark_text"
|
||||
android:textStyle="bold"
|
||||
android:textSize="24sp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hello" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/moneybox_val_tv"
|
||||
app:layout_constraintTop_toBottomOf="@id/plan_val_tv"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:textColor="@color/dark_text"
|
||||
android:layout_margin="12dp"
|
||||
android:textStyle="bold"
|
||||
android:textSize="24sp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Total Plan: £7000" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_add_20"
|
||||
style="@style/Widget.Button.Colored.Rounded"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/add_20"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/moneybox_val_tv"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
81
app/src/main/res/layout/products_list_item.xml
Normal file
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="@color/light_grey"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="12dp"
|
||||
android:padding="12dp"
|
||||
app:cardCornerRadius="22dp"
|
||||
tools:layout_height="150dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:padding="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/name_tv"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/vertical_spacing_text"
|
||||
tools:text="Stocks & Shares ISA"
|
||||
android:textColor="@color/colorAccent"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/icon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/plan_val_tv"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/vertical_spacing_text"
|
||||
tools:text="Plan Value: £4000"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/colorAccent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/name_tv" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/moneybox_val_tv"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/vertical_spacing_text"
|
||||
tools:text="Moneybox: £4000"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/colorAccent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/plan_val_tv" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="12dp"
|
||||
android:gravity="center"
|
||||
android:text=">"
|
||||
android:textColor="@color/colorPrimaryDark"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</FrameLayout>
|
||||
56
app/src/main/res/layout/user_accounts_fragment.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="@color/light_grey"
|
||||
android:id="@+id/useraccounts"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_name_tv"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_margin="12dp"
|
||||
android:textColor="@color/dark_text"
|
||||
android:textStyle="bold"
|
||||
android:textSize="24sp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hello" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total_plan_val_tv"
|
||||
app:layout_constraintTop_toBottomOf="@id/user_name_tv"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:textColor="@color/dark_text"
|
||||
android:layout_margin="12dp"
|
||||
android:textStyle="bold"
|
||||
android:textSize="24sp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Total Plan: £7000" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_layout"
|
||||
app:layout_constraintTop_toBottomOf="@id/total_plan_val_tv"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:textStyle="bold"
|
||||
android:textSize="24sp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="12dp"
|
||||
tools:text="Total Plan: £7000"
|
||||
android:orientation="vertical">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/products_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/products_list_item"/>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
13
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">@color/white</color>
|
||||
<color name="colorPrimaryDark">#546270</color>
|
||||
<color name="colorAccent">#49bfbd</color>
|
||||
<color name="white">#ffffff</color>
|
||||
<color name="light_grey">#dde0e2</color>
|
||||
|
||||
<color name="dark_text">#006179</color>
|
||||
#006179
|
||||
|
||||
|
||||
</resources>
|
||||
7
app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
|
||||
<dimen name="vertical_spacing_text">6dp</dimen>
|
||||
</resources>
|
||||
20
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<resources>
|
||||
<string name="app_name">MiniMoneybox</string>
|
||||
<!-- Strings related to login -->
|
||||
<string name="prompt_email">Email</string>
|
||||
<string name="prompt_password">Password</string>
|
||||
<string name="prompt_full_name">Full Name (optional)</string>
|
||||
<string name="action_sign_in">Sign in</string>
|
||||
<string name="action_sign_in_short">Sign in</string>
|
||||
<string name="error_incorrect_password">This password is incorrect</string>
|
||||
<string name="email_address_error">Please enter a correct email address</string>
|
||||
<string name="password_error">Please enter a correct password</string>
|
||||
<string name="full_name_error">Please enter your full name</string>
|
||||
<string name="input_valid">Input is valid!</string>
|
||||
<string name="hello">Hello !</string>
|
||||
|
||||
<!-- TODO: Remove or change this placeholder text -->
|
||||
<string name="hello_blank_fragment">Hello blank fragment</string>
|
||||
<string name="add_20">Add £20</string>
|
||||
<string name="session_expired">Your session has expired. Please close the app and log in again.</string>
|
||||
</resources>
|
||||
26
app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Button.Colored.Rounded" parent="Widget.AppCompat.Button.Borderless.Colored">
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:layout_height">44dp</item>
|
||||
<item name="android:background">@drawable/background_button_colored_rounded_medium</item>
|
||||
<item name="android:textColor">@color/white</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Moneybox.EditText" parent="Widget.AppCompat.EditText">
|
||||
<item name="android:background">@drawable/background_edit_text</item>
|
||||
<item name="android:lineSpacingExtra">8sp</item>
|
||||
<item name="android:textColor">@color/colorPrimaryDark</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.example.minimoneybox
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
9
app/src/test/java/com/example/minimoneybox/TestUtils.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.example.minimoneybox
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.example.minimoneybox.ui.OneTimeObserver
|
||||
|
||||
fun <T> LiveData<T>.observeOnce(onChangeHandler: (T) -> Unit) {
|
||||
val observer = OneTimeObserver(handler = onChangeHandler)
|
||||
observe(observer, observer)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.example.minimoneybox.data.repository
|
||||
|
||||
import com.example.minimoneybox.data.network.LoginApi
|
||||
import com.example.minimoneybox.data.network.UserAccountApi
|
||||
import com.example.minimoneybox.data.network.response.LoginResponse
|
||||
import com.example.minimoneybox.data.network.response.PaymentResponse
|
||||
import com.example.minimoneybox.data.network.response.ProductApiResponse
|
||||
import com.example.minimoneybox.data.prefs.KeystoreStorage
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.ResponseBody
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
|
||||
class RepositoryNetworkTest{
|
||||
|
||||
lateinit var repository: MoneyBoxRepository
|
||||
|
||||
@Mock
|
||||
lateinit var api: LoginApi
|
||||
@Mock
|
||||
lateinit var securePrefs: KeystoreStorage
|
||||
@Mock
|
||||
lateinit var userAccountApi: UserAccountApi
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
repository = MoneyBoxRepositoryImpl(securePrefs, api, userAccountApi, Gson())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginUser_positiveResponse() = runBlocking {
|
||||
//GIVEN - Create the hashmap used in the function
|
||||
val hash = HashMap<String, String>()
|
||||
hash["Email"] = "email"
|
||||
hash["Password"] = "password"
|
||||
hash["Idfa"] = "ANYTHING"
|
||||
//create a successful retrofit response
|
||||
val mockLoginResponse = mock(LoginResponse::class.java)
|
||||
val re = Response.success(mockLoginResponse)
|
||||
|
||||
//WHEN - loginApiRequest to return a successful response
|
||||
Mockito.`when`(api.loginApiRequest(hash)).thenReturn(re)
|
||||
|
||||
//THEN - the unwrapped login response contains the correct values
|
||||
val login = repository.loginUser("email","password")
|
||||
assertNotNull(login)
|
||||
assertEquals(login, mockLoginResponse)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getInvestmentProducts_positiveResponse() = runBlocking {
|
||||
//GIVEN
|
||||
val mockProductsResponse = mock(ProductApiResponse::class.java)
|
||||
val re = Response.success(mockProductsResponse)
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(userAccountApi.getProductsFromApi("Bearer ")).thenReturn(re)
|
||||
|
||||
//THEN
|
||||
val products = repository.getProducts("")
|
||||
assertNotNull(products)
|
||||
assertEquals(products, mockProductsResponse)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pushOneOffPayment_positiveResponse() = runBlocking {
|
||||
//GIVEN
|
||||
val hash = HashMap<String, Int>()
|
||||
hash["Amount"] = 20
|
||||
hash["InvestorProductId"] = 1674
|
||||
val mockPaymentsResponse = mock(PaymentResponse::class.java)
|
||||
val re = Response.success(mockPaymentsResponse)
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(userAccountApi.oneOffPaymentsFromApi(hash, "Bearer 133nhdk12l58dmHJBNmd=")).thenReturn(re)
|
||||
|
||||
//THEN
|
||||
val products = repository.oneOffPayment(1674,"133nhdk12l58dmHJBNmd=")
|
||||
assertNotNull(products)
|
||||
assertEquals(products, mockPaymentsResponse)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginUser_negativeResponse() = runBlocking {
|
||||
//GIVEN
|
||||
val hash = HashMap<String, String>()
|
||||
hash["Email"] = "email"
|
||||
hash["Password"] = "password"
|
||||
hash["Idfa"] = "ANYTHING"
|
||||
//mock retrofit error response
|
||||
val mockBody = mock(ResponseBody::class.java)
|
||||
val mockRaw = mock(okhttp3.Response::class.java)
|
||||
val re = Response.error<String>(mockBody, mockRaw)
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(api.loginApiRequest(hash)).then { re }
|
||||
|
||||
//THEN - assert exception is not null
|
||||
val ioExceptionReturned= assertFailsWith<IOException> {
|
||||
repository.loginUser("email","password")
|
||||
}
|
||||
assertNotNull(ioExceptionReturned)
|
||||
assertNotNull(ioExceptionReturned.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getInvestmentProducts_negativeResponse() = runBlocking {
|
||||
//GIVEN
|
||||
val mockBody = mock(ResponseBody::class.java)
|
||||
val mockRaw = mock(okhttp3.Response::class.java)
|
||||
val re = Response.error<String>(mockBody, mockRaw)
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(userAccountApi.getProductsFromApi("Bearer ")).then { re }
|
||||
|
||||
//THEN
|
||||
val ioExceptionReturned= assertFailsWith<IOException> {
|
||||
repository.getProducts("")
|
||||
}
|
||||
|
||||
assertNotNull(ioExceptionReturned)
|
||||
assertNotNull(ioExceptionReturned.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pushOneOffPayment_negativeResponse() = runBlocking {
|
||||
//GIVEN
|
||||
val hash = HashMap<String, Int>()
|
||||
hash["Amount"] = 20
|
||||
hash["InvestorProductId"] = 1674
|
||||
|
||||
val mockBody = mock(ResponseBody::class.java)
|
||||
val mockRaw = mock(okhttp3.Response::class.java)
|
||||
val re = Response.error<String>(mockBody, mockRaw)
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(userAccountApi.oneOffPaymentsFromApi(hash, "Bearer ")).then { re }
|
||||
|
||||
//THEN
|
||||
val ioExceptionReturned= assertFailsWith<IOException> {
|
||||
repository.oneOffPayment(1674,"")
|
||||
}
|
||||
|
||||
assertNotNull(ioExceptionReturned)
|
||||
assertNotNull(ioExceptionReturned.message)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.example.minimoneybox.data.repository
|
||||
|
||||
import com.example.minimoneybox.data.network.LoginApi
|
||||
import com.example.minimoneybox.data.network.UserAccountApi
|
||||
import com.example.minimoneybox.data.prefs.KeystoreStorage
|
||||
import com.google.gson.Gson
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
class RepositoryStorageTest {
|
||||
|
||||
lateinit var repository: MoneyBoxRepository
|
||||
|
||||
@Mock
|
||||
lateinit var api: LoginApi
|
||||
@Mock
|
||||
lateinit var securePrefs: KeystoreStorage
|
||||
@Mock
|
||||
lateinit var userAccountApi: UserAccountApi
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
repository = MoneyBoxRepositoryImpl(securePrefs, api, userAccountApi, Gson())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveAndRetrieve_PositiveResponse() {
|
||||
//GIVEN
|
||||
val token = "92RG9qYRq5sUUaoJuolORfZORb9G6n014X2I="
|
||||
repository.saveAuthToken(token)
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(securePrefs.loadTokenFromPrefs()).thenReturn(token)
|
||||
|
||||
//THEN
|
||||
assertEquals(token, repository.loadAuthToken())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveAndRetrieveCredentials_PositiveResponse() {
|
||||
//GIVEN
|
||||
val pair: Pair<String, String> = Pair("forename", "Surname")
|
||||
repository.saveCredentials("forename", "Surname")
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(securePrefs.loadCredentialsFromPrefs()).thenReturn(pair)
|
||||
|
||||
//THEN
|
||||
assertEquals(pair, repository.loadCredentials())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.minimoneybox.ui
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.lifecycle.Observer
|
||||
|
||||
class OneTimeObserver<T>(private val handler: (T) -> Unit) : Observer<T>, LifecycleOwner {
|
||||
private val lifecycle = LifecycleRegistry(this)
|
||||
|
||||
init {
|
||||
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
}
|
||||
|
||||
override fun getLifecycle(): Lifecycle = lifecycle
|
||||
|
||||
override fun onChanged(t: T) {
|
||||
handler(t)
|
||||
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.example.minimoneybox.ui.login
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.example.minimoneybox.data.models.SessionObject
|
||||
import com.example.minimoneybox.data.network.response.LoginResponse
|
||||
import com.example.minimoneybox.data.repository.MoneyBoxRepository
|
||||
import com.example.minimoneybox.observeOnce
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
class LoginViewModelTest {
|
||||
|
||||
// Run tasks synchronously
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
lateinit var viewModel: LoginViewModel
|
||||
|
||||
@Mock
|
||||
lateinit var repository: MoneyBoxRepository
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
viewModel = LoginViewModel(repository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun attemptLogin_validCredentials_successResponse() = runBlocking{
|
||||
//GIVEN
|
||||
val sessionObject = SessionObject()
|
||||
sessionObject.bearerToken = "123456"
|
||||
val loginResponse = LoginResponse(sessionObject)
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(repository.loginUser("email","password"))
|
||||
.thenReturn(loginResponse)
|
||||
|
||||
//THEN
|
||||
viewModel.operationSuccess.observeOnce {
|
||||
print(it.toString())
|
||||
assertEquals(true, it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun attemptLogin_validCredentials_unsuccessfulResponse() = runBlocking{
|
||||
//GIVEN
|
||||
val errorMessage = "Could not login user"
|
||||
//WHEN
|
||||
Mockito.`when`(repository.loginUser("email","password"))
|
||||
.thenAnswer { throw IOException() }
|
||||
|
||||
//THEN
|
||||
viewModel.operationFailed.observeOnce {
|
||||
assertEquals(errorMessage, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.example.minimoneybox.ui.user
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.example.minimoneybox.data.network.response.PaymentResponse
|
||||
import com.example.minimoneybox.data.network.response.ProductApiResponse
|
||||
import com.example.minimoneybox.data.repository.MoneyBoxRepository
|
||||
import com.example.minimoneybox.observeOnce
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.io.IOException
|
||||
|
||||
class UserAccountsViewModelTest{
|
||||
// Run tasks synchronously
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
lateinit var viewModel: UserAccountsViewModel
|
||||
|
||||
@Mock
|
||||
lateinit var repository: MoneyBoxRepository
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
viewModel = UserAccountsViewModel(repository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getProducts_validCredentials_successResponse() = runBlocking{
|
||||
//GIVEN
|
||||
val token = "123456"
|
||||
val product = mock(ProductApiResponse::class.java)
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(repository.loadAuthToken()).thenReturn(token)
|
||||
Mockito.`when`(repository.getProducts(token)).thenReturn(product)
|
||||
|
||||
//THEN
|
||||
viewModel.getInvestorProducts()
|
||||
viewModel.operationSuccess.observeOnce {
|
||||
assertEquals(true, it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getProducts_validCredentials_unsuccessfulResponse() = runBlocking{
|
||||
//GIVEN
|
||||
val token = "123456"
|
||||
val errorMessage = "Could not retrieve products"
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(repository.loadAuthToken()).thenReturn(token)
|
||||
Mockito.`when`(repository.getProducts(token)).thenAnswer { throw IOException(errorMessage) }
|
||||
|
||||
//THEN
|
||||
viewModel.getInvestorProducts()
|
||||
viewModel.operationFailed.observeOnce {
|
||||
assertEquals(errorMessage, it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getProducts_invalidCredentials_unsuccessfulResponse() = runBlocking{
|
||||
//GIVEN
|
||||
val errorMessage = "Failed to retrieve token"
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(repository.loadAuthToken()).thenReturn(null)
|
||||
|
||||
//THEN
|
||||
viewModel.getInvestorProducts()
|
||||
viewModel.operationFailed.observeOnce {
|
||||
assertEquals(errorMessage, it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneOffPayment_validCredentials_successResponse() = runBlocking{
|
||||
//GIVEN
|
||||
val productId = 2020
|
||||
val token = "123456"
|
||||
val product = mock(PaymentResponse::class.java)
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(repository.loadAuthToken()).thenReturn(token)
|
||||
Mockito.`when`(repository.oneOffPayment(productId,token)).thenReturn(product)
|
||||
|
||||
//THEN
|
||||
viewModel.oneOffPayment(2020)
|
||||
viewModel.operationSuccess.observeOnce {
|
||||
assertEquals(true, it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneOffPayment_validCredentials_unsuccessfulResponse() = runBlocking{
|
||||
//GIVEN
|
||||
val productId = 2020
|
||||
val token = "123456"
|
||||
val errorMessage = "Could not post payment"
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(repository.loadAuthToken()).thenReturn(token)
|
||||
Mockito.`when`(repository.oneOffPayment(productId,token)).thenAnswer { throw IOException(errorMessage)}
|
||||
|
||||
//THEN
|
||||
viewModel.oneOffPayment(2020)
|
||||
viewModel.operationSuccess.observeOnce {
|
||||
assertEquals(false, it)
|
||||
}
|
||||
viewModel.operationFailed.observeOnce {
|
||||
print(it)
|
||||
assertEquals(errorMessage, it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneOffPayment_invalidCredentials_unsuccessfulResponse() = runBlocking{
|
||||
//GIVEN
|
||||
val errorMessage = "Failed to retrieve token"
|
||||
val token = ""
|
||||
val productId = 2020
|
||||
|
||||
//WHEN
|
||||
Mockito.`when`(repository.oneOffPayment(productId, token)).thenReturn(null)
|
||||
|
||||
//THEN
|
||||
viewModel.oneOffPayment(productId)
|
||||
viewModel.operationFailed.observeOnce {
|
||||
assertEquals(errorMessage, it)
|
||||
}
|
||||
}
|
||||
}
|
||||