Initial commit
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
/.idea/*
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
80
README.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# MoneyBox Android Technical Task
|
||||||
|
|
||||||
|
A small application for viewing investment products.
|
||||||
|
|
||||||
|
# Part A
|
||||||
|
|
||||||
|
### Bug 1 - Layout does not look as expected
|
||||||
|
|
||||||
|
- Constraints added to all TextInputLayouts
|
||||||
|
- Edit text views within TextInputLayouts and TextInputLayouts within match parent and the TextInputLayouts match parents
|
||||||
|
- correct spacing between edges margin of 12dp left and right
|
||||||
|
- email TextInputLayout with a 48dp margin at the top
|
||||||
|
|
||||||
|
### Bug 2 - Validation is incorrect
|
||||||
|
|
||||||
|
- Email matching android email matcher
|
||||||
|
- allValid within allFieldsValid() is true by default
|
||||||
|
- once allValid changes to false it remains false and no login called
|
||||||
|
- Changes to false every failed validation
|
||||||
|
|
||||||
|
### Bug 3 - Animation is looping incorrectly
|
||||||
|
- set minimum and maximum frames based the firstAnim Pair<Int, Int> range respectively
|
||||||
|
- set an animation completion listener and wait for first animation play to finish
|
||||||
|
- on completion of first animation change the min and max frames based on secondAnim Pair<Int, Int> range
|
||||||
|
- play animation with new min and max frames
|
||||||
|
|
||||||
|
# Part B
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Minimum android SDK version 21, Android 5.0.0 (Lollipop)
|
||||||
|
Permissions : Internet, Network State
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Login (Name optional)
|
||||||
|
- View investment products
|
||||||
|
- Add one off payment of £20 to an investment
|
||||||
|
|
||||||
|
|
||||||
|
## Architectural Pattern
|
||||||
|
|
||||||
|
MVVM - Model View Viewmodel
|
||||||
|
SOLID coding
|
||||||
|
|
||||||
|
## Jetpack
|
||||||
|
|
||||||
|
* [AndroidX](https://developer.android.com/jetpack)
|
||||||
|
|
||||||
|
## Unit tests
|
||||||
|
|
||||||
|
### Test case one
|
||||||
|
- Respository Unit test (Networkings)
|
||||||
|
|
||||||
|
### Test case two
|
||||||
|
- Repository Unit test (Storage)
|
||||||
|
|
||||||
|
### Test case one
|
||||||
|
- Login viewmodel test
|
||||||
|
|
||||||
|
### Test case two
|
||||||
|
- UserAccount viewmodel test
|
||||||
|
|
||||||
|
## Integration tests
|
||||||
|
|
||||||
|
### Test case one
|
||||||
|
- LoginActivity UI test
|
||||||
|
|
||||||
|
## Built With
|
||||||
|
|
||||||
|
* [Kodein](https://github.com/Kodein-Framework/Kodein-DI) - Painless Kotlin Dependency Injection
|
||||||
|
* [Retrofit](https://github.com/square/retrofit) - Type-safe HTTP client for Android and Java by Square, Inc
|
||||||
|
* [Secured Preference Store](https://github.com/iamMehedi/Secured-Preference-Store) - A SharedPreferences wrapper for Android that encrypts the content with 256 bit AES encryption
|
||||||
|
* [Lottie](https://github.com/airbnb/lottie-android) - Lottie is a mobile library for Android and iOS that parses Adobe After Effects animations exported as json with Bodymovin and renders them natively on mobile!
|
||||||
|
|
||||||
|
## Submitted by
|
||||||
|
|
||||||
|
* **Haider Malik** - *Android Developer*
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
build.gradle
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.3.61'
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
17
gradle.properties
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#Wed Mar 06 09:53:14 GMT 2019
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
||||||
172
gradlew
vendored
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=$(save "$@")
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
|
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
84
gradlew.bat
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
1
settings.gradle
Normal file
@@ -0,0 +1 @@
|
|||||||
|
include ':app'
|
||||||