├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── rootstrap
│ │ │ │ └── android
│ │ │ │ ├── util
│ │ │ │ ├── Util.kt
│ │ │ │ ├── NetworkState.kt
│ │ │ │ ├── extensions
│ │ │ │ │ ├── Gson.kt
│ │ │ │ │ ├── ViewGroup.kt
│ │ │ │ │ ├── Validations.kt
│ │ │ │ │ ├── ImageView.kt
│ │ │ │ │ ├── EditText.kt
│ │ │ │ │ ├── ProgressBar.kt
│ │ │ │ │ ├── Fragment.kt
│ │ │ │ │ ├── ActionCallback.kt
│ │ │ │ │ └── Textview.kt
│ │ │ │ ├── dispatcher
│ │ │ │ │ ├── DispatcherProvider.kt
│ │ │ │ │ └── AppDispatcherProvider.kt
│ │ │ │ ├── LoadingDialogUtil.kt
│ │ │ │ ├── permissions
│ │ │ │ │ ├── PermissionManager.kt
│ │ │ │ │ ├── PermissionActivity.kt
│ │ │ │ │ └── PermissionFragment.kt
│ │ │ │ ├── ErrorUtil.kt
│ │ │ │ ├── Prefs.kt
│ │ │ │ └── UtilModule.kt
│ │ │ │ ├── models
│ │ │ │ └── Model.kt
│ │ │ │ ├── ui
│ │ │ │ ├── view
│ │ │ │ │ ├── IView.kt
│ │ │ │ │ ├── AuthView.kt
│ │ │ │ │ └── ProfileView.kt
│ │ │ │ ├── adapter
│ │ │ │ │ └── Adapter.kt
│ │ │ │ ├── custom
│ │ │ │ │ ├── CustomView.kt
│ │ │ │ │ ├── LoadingDialog.kt
│ │ │ │ │ └── CustomTypefaceSpan.kt
│ │ │ │ ├── base
│ │ │ │ │ ├── BaseView.kt
│ │ │ │ │ ├── BaseActivity.kt
│ │ │ │ │ ├── BaseViewModel.kt
│ │ │ │ │ ├── BaseNavActivity.kt
│ │ │ │ │ └── BaseFragment.kt
│ │ │ │ ├── activity
│ │ │ │ │ ├── MainActivity.kt
│ │ │ │ │ └── OnBoardingActivity.kt
│ │ │ │ ├── viewmodel
│ │ │ │ │ ├── ProfileViewModel.kt
│ │ │ │ │ ├── SignInViewModel.kt
│ │ │ │ │ └── SignUpViewModel.kt
│ │ │ │ └── fragment
│ │ │ │ │ ├── ProfileFragment.kt
│ │ │ │ │ ├── SignInFragment.kt
│ │ │ │ │ └── SignUpFragment.kt
│ │ │ │ ├── database
│ │ │ │ └── AppDataBase.kt
│ │ │ │ ├── network
│ │ │ │ ├── models
│ │ │ │ │ ├── ErrorModel.kt
│ │ │ │ │ └── User.kt
│ │ │ │ ├── managers
│ │ │ │ │ ├── session
│ │ │ │ │ │ ├── SessionManager.kt
│ │ │ │ │ │ └── SessionManagerImpl.kt
│ │ │ │ │ ├── user
│ │ │ │ │ │ ├── UserManager.kt
│ │ │ │ │ │ └── UserManagerImpl.kt
│ │ │ │ │ └── ManagerModule.kt
│ │ │ │ ├── services
│ │ │ │ │ ├── ApiModule.kt
│ │ │ │ │ ├── ApiService.kt
│ │ │ │ │ ├── HeadersInterceptor.kt
│ │ │ │ │ ├── AuthenticationInterceptor.kt
│ │ │ │ │ └── ResponseInterceptor.kt
│ │ │ │ └── providers
│ │ │ │ │ └── ServiceProviderModule.kt
│ │ │ │ ├── metrics
│ │ │ │ ├── MetricsEvents.kt
│ │ │ │ ├── base
│ │ │ │ │ ├── BaseAnalytics.kt
│ │ │ │ │ └── Provider.kt
│ │ │ │ ├── Analytics.kt
│ │ │ │ ├── Events.kt
│ │ │ │ ├── GoogleAnalytics.kt
│ │ │ │ └── MixPanelAnalytics.kt
│ │ │ │ └── App.kt
│ │ ├── res
│ │ │ ├── xml
│ │ │ │ └── network_security_config.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ │ ├── colors.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── styles.xml
│ │ │ │ └── strings.xml
│ │ │ ├── drawable
│ │ │ │ ├── background_loader.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── fragment_sample.xml
│ │ │ │ ├── view_loading_dialog.xml
│ │ │ │ ├── activity_base_nav.xml
│ │ │ │ ├── fragment_profile.xml
│ │ │ │ ├── fragment_sign_in.xml
│ │ │ │ └── fragment_sign_up.xml
│ │ │ ├── navigation
│ │ │ │ ├── nav_main.xml
│ │ │ │ └── nav_onboarding.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ └── AndroidManifest.xml
│ ├── dev
│ │ └── res
│ │ │ └── xml
│ │ │ └── network_security_config.xml
│ ├── staging
│ │ └── res
│ │ │ └── xml
│ │ │ └── network_security_config.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── rootstrap
│ │ │ └── android
│ │ │ ├── ValidationTests.kt
│ │ │ ├── test
│ │ │ ├── UnitTestBase.kt
│ │ │ └── TestDispatcherProvider.kt
│ │ │ ├── SignUpActivityViewModelTest.kt
│ │ │ └── SignInActivityViewModelTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── rootstrap
│ │ └── android
│ │ ├── CustomTestRunner.kt
│ │ ├── tests
│ │ ├── utils
│ │ │ └── PrefTests.kt
│ │ ├── ProfileFragmentTest.kt
│ │ ├── SignInFragmentTest.kt
│ │ └── SignUpFragmentTest.kt
│ │ └── utils
│ │ ├── MockServer.kt
│ │ └── BaseTests.kt
├── proguard-rules.pro
├── google-services.json
└── build.gradle
├── settings.gradle
├── gradle.properties.secret
├── .gitsecret
├── keys
│ ├── pubring.kbx
│ ├── trustdb.gpg
│ └── pubring.kbx~
└── paths
│ └── mapping.cfg
├── secure
├── key.keystore.secret
├── google-api.json.secret
└── Readme.md
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── encodings.xml
├── codeStyles
│ └── codeStyleConfig.xml
├── vcs.xml
└── gradle.xml
├── fastlane
├── Pluginfile
├── Appfile
├── README.md
└── Fastfile
├── CODEOWNERS
├── Gemfile
├── .editorconfig
├── pull_request_template.md
├── pre-commit
├── .gitignore
├── gradle.properties
├── .github
└── workflows
│ └── cicd.yml
├── gradlew.bat
├── Gemfile.lock
├── gradlew
├── README.md
└── default-detekt-config.yml
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/gradle.properties.secret:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/gradle.properties.secret
--------------------------------------------------------------------------------
/.gitsecret/keys/pubring.kbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/.gitsecret/keys/pubring.kbx
--------------------------------------------------------------------------------
/.gitsecret/keys/trustdb.gpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/.gitsecret/keys/trustdb.gpg
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/Util.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util
2 |
3 | class Util
4 |
--------------------------------------------------------------------------------
/secure/key.keystore.secret:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/secure/key.keystore.secret
--------------------------------------------------------------------------------
/.gitsecret/keys/pubring.kbx~:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/.gitsecret/keys/pubring.kbx~
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/models/Model.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.models
2 |
3 | class Model
4 |
--------------------------------------------------------------------------------
/secure/google-api.json.secret:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/secure/google-api.json.secret
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/view/IView.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.view
2 |
3 | class IView
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/adapter/Adapter.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.adapter
2 |
3 | class Adapter
4 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/custom/CustomView.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.custom
2 |
3 | class CustomView
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/database/AppDataBase.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.database
2 |
3 | class AppDataBase {
4 | // TODO
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rootstrap/android-base/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/fastlane/Pluginfile:
--------------------------------------------------------------------------------
1 | # Autogenerated by fastlane
2 | #
3 | # Ensure this file is checked in to source control!
4 |
5 | gem 'fastlane-plugin-increment_version_code'
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/NetworkState.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util
2 |
3 | enum class NetworkState {
4 | loading,
5 | idle,
6 | error
7 | }
8 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # These owners will be the default owners for everything in
2 | # the repo. Unless a later match takes precedence
3 |
4 | * @ximenaperez @mato2593 @jjszolno @amaury901130
5 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "fastlane"
4 |
5 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
6 | eval_gemfile(plugins_path) if File.exist?(plugins_path)
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/view/AuthView.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.view
2 |
3 | import com.rootstrap.android.ui.base.BaseView
4 |
5 | interface AuthView : BaseView {
6 |
7 | fun showProfile()
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/view/ProfileView.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.view
2 |
3 | import com.rootstrap.android.ui.base.BaseView
4 |
5 | interface ProfileView : BaseView {
6 |
7 | fun goToFirstScreen()
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/base/BaseView.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.base
2 |
3 | interface BaseView {
4 |
5 | fun showProgress()
6 |
7 | fun hideProgress()
8 |
9 | fun showError(message: String?)
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{kt,kts}]
2 | # Comma-separated list of rules to disable (Since 0.34.0)
3 | # Note that rules in any ruleset other than the standard ruleset will need to be prefixed
4 | # by the ruleset identifier.
5 | disabled_rules=import-ordering,experimental:annotation
6 |
--------------------------------------------------------------------------------
/.gitsecret/paths/mapping.cfg:
--------------------------------------------------------------------------------
1 | secure/google-api.json:e494b2376306f39ea643ebe13156e4b885cddb64755a3a3fe4dd5f2807cb9a70
2 | secure/key.keystore:6c70718b97803ac753020362b214a40762dc4ed95c4665648f86758c20ddbae9
3 | gradle.properties:2a428ac440037c87d1001976c703d0ea412dee852aae78d9827f1307c4604545
4 |
--------------------------------------------------------------------------------
/app/src/dev/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | localhost
5 |
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Jan 18 09:20:52 ART 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
7 |
--------------------------------------------------------------------------------
/app/src/staging/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | localhost
5 |
6 |
7 |
--------------------------------------------------------------------------------
/pull_request_template.md:
--------------------------------------------------------------------------------
1 | #### ISSUE[#]
2 | * Issue title
3 |
4 | ---
5 |
6 | #### Description
7 | *
8 |
9 | ---
10 |
11 | #### Tasks
12 | *
13 |
14 | ---
15 |
16 | #### Risk
17 | *
18 |
19 | ---
20 |
21 | #### Notes
22 | *
23 |
24 | ---
25 |
26 | #### Preview
27 | * Screen shots
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/extensions/Gson.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.extensions
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.reflect.TypeToken
5 |
6 | inline fun Gson.fromJson(json: String) = this.fromJson(json, object : TypeToken() {}.type)
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/activity/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.activity
2 |
3 | import com.rootstrap.android.R
4 | import com.rootstrap.android.ui.base.BaseNavActivity
5 |
6 | class MainActivity : BaseNavActivity() {
7 | override var navGraph: Int? = R.navigation.nav_main
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background_loader.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/activity/OnBoardingActivity.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.activity
2 |
3 | import com.rootstrap.android.R
4 | import com.rootstrap.android.ui.base.BaseNavActivity
5 |
6 | class OnBoardingActivity : BaseNavActivity() {
7 | override var navGraph: Int? = R.navigation.nav_onboarding
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/models/ErrorModel.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.models
2 |
3 | import com.squareup.moshi.Json
4 |
5 | data class ErrorModel(
6 | @Json(name = "errors") val errors: Any?,
7 | @Json(name = "error") val error: String?
8 | )
9 |
10 | data class ErrorModelSerializer(val error: ErrorModel)
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/metrics/MetricsEvents.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.metrics
2 |
3 | // sample events
4 | const val LOGIN = "logIn"
5 | const val LOGOUT = "logOut"
6 | const val SIGNUP = "signUp"
7 | const val VISIT_SIGN_IN = "visit_sign_in"
8 | const val VISIT_PROFILE = "visit_profile"
9 | const val VISIT_SIGN_UP = "visit_sign_up"
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/dispatcher/DispatcherProvider.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.dispatcher
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 |
5 | interface DispatcherProvider {
6 | val io: CoroutineDispatcher
7 | val default: CoroutineDispatcher
8 | val main: CoroutineDispatcher
9 | val unconfined: CoroutineDispatcher
10 | }
11 |
--------------------------------------------------------------------------------
/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "Running git pre-commit hook"
4 |
5 | ./gradlew ktlintFormat
6 | git add .
7 |
8 | git stash -q --keep-index
9 |
10 | ./gradlew ktlint
11 |
12 | RESULT=$?
13 |
14 | git stash pop -q
15 |
16 | # return 1 exit code if running checks fails
17 | [ $RESULT -ne 0 ] && echo "Please fix the remaining issues before commiting" && exit 1
18 | echo "😎" && exit 0
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.base
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.appcompat.app.AppCompatActivity
5 | import dagger.hilt.android.AndroidEntryPoint
6 |
7 | @SuppressLint("Registered")
8 | @AndroidEntryPoint
9 | open class BaseActivity : AppCompatActivity() {
10 | // TODO:.. add common functions
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 30dp
5 | 10dp
6 |
7 |
8 | 20dp
9 | 45dp
10 | 48dp
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/managers/session/SessionManager.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.managers.session
2 |
3 | import com.rootstrap.android.network.models.User
4 |
5 | interface SessionManager {
6 | var user: User?
7 | fun addAuthenticationHeaders(accessToken: String, client: String, uid: String)
8 | fun signOut()
9 | fun signIn(user: User)
10 | fun isUserSignedIn(): Boolean
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/metrics/base/BaseAnalytics.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.metrics.base
2 |
3 | /**
4 | * Add custom metrics
5 | * */
6 | interface BaseAnalytics {
7 | fun addProvider(provider: Provider)
8 | fun addProviders(providers: ArrayList)
9 | fun identifyUser()
10 | fun track(event: TrackEvent)
11 | fun addOrEditProperty(property: UserProperty)
12 | fun addOrEditProperties(properties: List)
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /gradle.properties
5 | /.idea/caches
6 | /.idea/libraries
7 | /.idea/modules.xml
8 | /.idea/workspace.xml
9 | /.idea/navEditor.xml
10 | /.idea/assetWizardSettings.xml
11 | /.idea/gradle.xml
12 | /.idea/*
13 | /app/prod/release/*
14 | /fastlane/report.xml
15 | .DS_Store
16 | /build
17 | /captures
18 | .externalNativeBuild
19 | .gitsecret/keys/random_seed
20 | fastlane/report.xml
21 | !*.secret
22 | /secure/key.keystore
23 | /secure/google-api.json
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/extensions/ViewGroup.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.extensions
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 |
6 | /**
7 | * Layout Inflater for Recycler View adapters
8 | * @param layout [Int] Layout xlm
9 | * */
10 | fun ViewGroup.inflate(layout: Int, attachToRoot: Boolean = false) =
11 | LayoutInflater.from(context).inflate(layout, this, attachToRoot)
12 |
13 | /**
14 | * Add more extensions in case you need it
15 | * */
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/managers/user/UserManager.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.managers.user
2 |
3 | import com.rootstrap.android.network.models.User
4 | import com.rootstrap.android.network.models.UserSerializer
5 | import com.rootstrap.android.util.extensions.Data
6 |
7 | interface UserManager {
8 | suspend fun signUp(user: User): Result>
9 | suspend fun signIn(user: User): Result>
10 | suspend fun signOut(): Result>
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/rootstrap/android/ValidationTests.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android
2 |
3 | import com.rootstrap.android.util.extensions.isEmail
4 | import org.junit.Assert.assertEquals
5 | import org.junit.Test
6 |
7 | class ValidationTests {
8 | @Test
9 | fun checkEmailTest() {
10 | assertEquals(true, "email@mkdi.com".isEmail())
11 | assertEquals(false, "email@mkdi".isEmail())
12 | assertEquals(false, "email".isEmail())
13 | assertEquals(false, "email.com".isEmail())
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/services/ApiModule.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.services
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import retrofit2.Retrofit
8 |
9 | @Module
10 | @InstallIn(SingletonComponent::class)
11 | class ApiModule {
12 |
13 | @Provides
14 | fun provideApiService(retrofit: Retrofit): ApiService {
15 | return retrofit.create(ApiService::class.java)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/rootstrap/android/CustomTestRunner.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import androidx.test.runner.AndroidJUnitRunner
6 | import dagger.hilt.android.testing.HiltTestApplication
7 |
8 | class CustomTestRunner : AndroidJUnitRunner() {
9 |
10 | override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
11 | return super.newApplication(cl, HiltTestApplication::class.java.name, context)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/test/java/com/rootstrap/android/test/UnitTestBase.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.test
2 |
3 | import androidx.annotation.CallSuper
4 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
5 | import io.mockk.MockKAnnotations
6 | import org.junit.Before
7 | import org.junit.Rule
8 |
9 | abstract class UnitTestBase {
10 |
11 | @get:Rule
12 | val instantExecutorRule = InstantTaskExecutorRule()
13 |
14 | @CallSuper
15 | @Before
16 | open fun setup() {
17 | MockKAnnotations.init(this)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/dispatcher/AppDispatcherProvider.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.dispatcher
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 |
6 | class AppDispatcherProvider : DispatcherProvider {
7 | override val io: CoroutineDispatcher = Dispatchers.IO
8 | override val default: CoroutineDispatcher = Dispatchers.Default
9 | override val main: CoroutineDispatcher = Dispatchers.Main
10 | override val unconfined: CoroutineDispatcher = Dispatchers.Unconfined
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/extensions/Validations.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.extensions
2 |
3 | fun String.validate(pattern: String): Boolean {
4 | return pattern.toRegex().matches(this)
5 | }
6 |
7 | fun String.isEmail(): Boolean {
8 | return validate(
9 | "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
10 | "\\@" +
11 | "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
12 | "(" +
13 | "\\." +
14 | "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
15 | ")+"
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/App.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android
2 |
3 | import android.app.Application
4 | import com.rootstrap.android.metrics.Analytics
5 | import com.rootstrap.android.metrics.GoogleAnalytics
6 | import dagger.hilt.android.HiltAndroidApp
7 |
8 | @HiltAndroidApp
9 | class App : Application() {
10 |
11 | override fun onCreate() {
12 | super.onCreate()
13 |
14 | Analytics.addProvider(GoogleAnalytics(this))
15 | // You need the api key in order to use MixPanel
16 | // Analytics.addProvider(MixPanelAnalytics(this))
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/extensions/ImageView.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.extensions
2 |
3 | import android.net.Uri
4 | import android.widget.ImageView
5 | import com.bumptech.glide.Glide
6 |
7 | /**
8 | * Using Glide to load an image
9 | * in case you change Glide by other library like Picasso or Fresco
10 | * just change this extension
11 | * @param uri [Uri] Image URI
12 | * */
13 | fun ImageView.loadUri(uri: Uri) {
14 | Glide.with(context)
15 | .load(uri)
16 | .into(this)
17 | }
18 |
19 | /**
20 | * Add more extensions in case you need it
21 | * */
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_sample.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/services/ApiService.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.services
2 |
3 | import com.rootstrap.android.network.models.UserSerializer
4 | import retrofit2.Call
5 | import retrofit2.http.Body
6 | import retrofit2.http.DELETE
7 | import retrofit2.http.POST
8 |
9 | interface ApiService {
10 |
11 | @POST("users/")
12 | fun signUp(@Body user: UserSerializer): Call
13 |
14 | @POST("users/sign_in")
15 | fun signIn(@Body user: UserSerializer): Call
16 |
17 | @DELETE("users/sign_out")
18 | fun signOut(): Call
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/models/User.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.models
2 |
3 | import com.squareup.moshi.Json
4 |
5 | data class User(
6 | @Json(name = "id") val id: String = "",
7 | @Json(name = "email") var email: String = "",
8 | @Json(name = "first_name") var firstName: String = "",
9 | @Json(name = "last_name") var lastName: String = "",
10 | @Json(name = "phone_number") var phone: String = "",
11 | @Json(name = "password") val password: String = "",
12 | @Json(name = "username") val username: String = ""
13 | )
14 |
15 | data class UserSerializer(@Json(name = "user") val user: User)
16 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/services/HeadersInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.services
2 |
3 | import okhttp3.Interceptor
4 | import okhttp3.Response
5 | import java.io.IOException
6 |
7 | class HeadersInterceptor : Interceptor {
8 |
9 | @Throws(IOException::class)
10 | override fun intercept(chain: Interceptor.Chain): Response {
11 | val request = chain.request()
12 | .newBuilder()
13 | .addHeader("Content-Type", "application/json")
14 | .addHeader("Accept", "application/json")
15 | .build()
16 |
17 | return chain.proceed(request)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_loading_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_base_nav.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/extensions/EditText.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.extensions
2 |
3 | import android.widget.EditText
4 | import java.util.regex.Pattern
5 |
6 | /**
7 | * [EditText] value
8 | * */
9 | fun EditText.value() = text.toString().trim()
10 |
11 | /**
12 | * Validate [EditText] with pattern
13 | * @param pattern [String] Pattern
14 | * */
15 | fun EditText.validate(pattern: String): Boolean =
16 | Pattern.compile(pattern, Pattern.CASE_INSENSITIVE)
17 | .matcher(value())
18 | .matches()
19 |
20 | /**
21 | * Validate [EditText] doesn't have a null value
22 | * */
23 | fun EditText.isNotEmpty() = value().isNotEmpty()
24 |
25 | /**
26 | * Add more extensions in case you need it
27 | * */
28 |
--------------------------------------------------------------------------------
/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | json_key_file("secure/google-api.json")
2 | # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
3 |
4 | for_platform :android do
5 | for_lane :deploy_production do
6 | package_name("com.rootstrap.android")
7 | end
8 |
9 | for_lane :debug_production do
10 | package_name("com.rootstrap.android")
11 | end
12 |
13 | for_lane :deploy_dev do
14 | package_name("com.rootstrap.android.dev")
15 | end
16 |
17 | for_lane :debug_dev do
18 | package_name("com.rootstrap.android.dev")
19 | end
20 |
21 | for_lane :deploy_staging do
22 | package_name("com.rootstrap.android.staging")
23 | end
24 |
25 | for_lane :debug_staging do
26 | package_name("com.rootstrap.android.staging")
27 | end
28 |
29 | end
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/custom/LoadingDialog.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.custom
2 |
3 | import android.app.Dialog
4 | import android.content.Context
5 | import android.content.DialogInterface
6 | import android.view.Window
7 | import com.rootstrap.android.R
8 |
9 | class LoadingDialog(context: Context, cancelListener: DialogInterface.OnCancelListener?) : Dialog(context) {
10 |
11 | init {
12 | requestWindowFeature(Window.FEATURE_NO_TITLE)
13 | setContentView(R.layout.view_loading_dialog)
14 | setCanceledOnTouchOutside(false)
15 |
16 | if (cancelListener != null) {
17 | setCancelable(true)
18 | setOnCancelListener(cancelListener)
19 | } else {
20 | setCancelable(false)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/test/java/com/rootstrap/android/test/TestDispatcherProvider.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.test
2 |
3 | import com.rootstrap.android.util.dispatcher.DispatcherProvider
4 | import kotlinx.coroutines.CoroutineDispatcher
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.test.TestCoroutineDispatcher
7 |
8 | @ExperimentalCoroutinesApi
9 | class TestDispatcherProvider(testCoroutineDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) :
10 | DispatcherProvider {
11 | override val default: CoroutineDispatcher = testCoroutineDispatcher
12 | override val main: CoroutineDispatcher = testCoroutineDispatcher
13 | override val io: CoroutineDispatcher = testCoroutineDispatcher
14 | override val unconfined: CoroutineDispatcher = testCoroutineDispatcher
15 | }
16 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/managers/ManagerModule.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.managers
2 |
3 | import com.rootstrap.android.network.managers.session.SessionManager
4 | import com.rootstrap.android.network.managers.session.SessionManagerImpl
5 | import com.rootstrap.android.network.managers.user.UserManager
6 | import com.rootstrap.android.network.managers.user.UserManagerImpl
7 | import dagger.Binds
8 | import dagger.Module
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import javax.inject.Singleton
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | abstract class ManagerModule {
16 |
17 | @Binds
18 | @Singleton
19 | abstract fun bindSessionManager(sessionManagerImpl: SessionManagerImpl): SessionManager
20 |
21 | @Binds
22 | @Singleton
23 | abstract fun bindUserManager(userManagerImplImpl: UserManagerImpl): UserManager
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/extensions/ProgressBar.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.extensions
2 |
3 | import android.animation.Animator
4 | import android.animation.ObjectAnimator
5 | import android.view.animation.DecelerateInterpolator
6 | import android.widget.ProgressBar
7 | import androidx.core.animation.addListener
8 |
9 | private const val DEFAULT_ANIMATION_TIME = 500L
10 |
11 | /**
12 | * Smoothly animates the progress of the progress bar to the specified value
13 | */
14 | fun ProgressBar.progressTo(
15 | newlyProgress: Int,
16 | timeInMillis: Long = DEFAULT_ANIMATION_TIME,
17 | onEndListener: ((Animator) -> Unit)? = null
18 | ) {
19 | ObjectAnimator.ofInt(this, "progress", progress, newlyProgress).apply {
20 | duration = timeInMillis
21 | interpolator = DecelerateInterpolator()
22 | setAutoCancel(true)
23 | onEndListener?.let {
24 | addListener(onEnd = it)
25 | }
26 | }.start()
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/managers/user/UserManagerImpl.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.managers.user
2 |
3 | import com.rootstrap.android.network.models.User
4 | import com.rootstrap.android.network.models.UserSerializer
5 | import com.rootstrap.android.network.services.ApiService
6 | import com.rootstrap.android.util.extensions.ActionCallback
7 | import com.rootstrap.android.util.extensions.Data
8 | import javax.inject.Inject
9 |
10 | /**
11 | * Singleton class
12 | * */
13 | class UserManagerImpl @Inject constructor(private val service: ApiService) : UserManager {
14 |
15 | override suspend fun signUp(user: User): Result> =
16 | ActionCallback.call(service.signUp(UserSerializer(user)))
17 |
18 | override suspend fun signIn(user: User): Result> =
19 | ActionCallback.call(service.signIn(UserSerializer(user)))
20 |
21 | override suspend fun signOut(): Result> =
22 | ActionCallback.call(service.signOut())
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/base/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.base
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleObserver
5 | import androidx.lifecycle.LiveData
6 | import androidx.lifecycle.MutableLiveData
7 | import androidx.lifecycle.OnLifecycleEvent
8 | import androidx.lifecycle.ViewModel
9 | import com.rootstrap.android.util.NetworkState
10 | import com.squareup.otto.Bus
11 |
12 | /**
13 | * A [ViewModel] base class
14 | * implement app general LiveData as Session or User
15 | * **/
16 | open class BaseViewModel : ViewModel(), LifecycleObserver {
17 | var error: String? = null
18 |
19 | protected val _networkState = MutableLiveData()
20 | val networkState: LiveData
21 | get() = _networkState
22 |
23 | @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
24 | fun register() = Bus().register(this)
25 |
26 | @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
27 | fun unregister() = Bus().unregister(this)
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/services/AuthenticationInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.services
2 |
3 | import com.rootstrap.android.util.Prefs
4 | import okhttp3.Interceptor
5 | import okhttp3.Response
6 | import java.io.IOException
7 | import javax.inject.Inject
8 |
9 | class AuthenticationInterceptor @Inject constructor(private val prefs: Prefs) : Interceptor {
10 |
11 | @Throws(IOException::class)
12 | override fun intercept(chain: Interceptor.Chain): Response {
13 | val builder = chain.request().newBuilder()
14 |
15 | if (hasHeaders()) {
16 | builder
17 | .addHeader(prefs.ACCESS_TOKEN, prefs.accessToken)
18 | .addHeader(prefs.CLIENT, prefs.client)
19 | .addHeader(prefs.UID, prefs.uid)
20 | }
21 |
22 | return chain.proceed(builder.build())
23 | }
24 |
25 | private fun hasHeaders(): Boolean {
26 | return prefs.accessToken != "" &&
27 | prefs.client != "" &&
28 | prefs.uid != ""
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/managers/session/SessionManagerImpl.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.managers.session
2 |
3 | import com.rootstrap.android.network.models.User
4 | import com.rootstrap.android.util.Prefs
5 | import javax.inject.Inject
6 |
7 | class SessionManagerImpl @Inject constructor(private val prefs: Prefs) : SessionManager {
8 |
9 | override var user: User? = prefs.user
10 | set(value) {
11 | field = value
12 | prefs.user = value
13 | }
14 |
15 | override fun addAuthenticationHeaders(accessToken: String, client: String, uid: String) {
16 | prefs.accessToken = accessToken
17 | prefs.client = client
18 | prefs.uid = uid
19 | }
20 |
21 | override fun signOut() {
22 | user = null
23 | prefs.clear()
24 | }
25 |
26 | override fun signIn(user: User) {
27 | this.user = user
28 | prefs.user = user
29 | prefs.signedIn = true
30 | }
31 |
32 | override fun isUserSignedIn(): Boolean {
33 | return (user != null && prefs.signedIn)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/rootstrap/android/tests/utils/PrefTests.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.tests.utils
2 |
3 | import android.content.SharedPreferences
4 | import androidx.security.crypto.EncryptedSharedPreferences
5 | import com.rootstrap.android.util.Prefs
6 | import dagger.hilt.android.testing.HiltAndroidRule
7 | import dagger.hilt.android.testing.HiltAndroidTest
8 | import org.junit.Assert
9 | import org.junit.Before
10 | import org.junit.Rule
11 | import org.junit.Test
12 | import java.util.* // ktlint-disable no-wildcard-imports
13 | import javax.inject.Inject
14 |
15 | @HiltAndroidTest
16 | class PrefTests {
17 | @get:Rule
18 | var hiltRule = HiltAndroidRule(this)
19 |
20 | @Inject
21 | lateinit var prefs: Prefs
22 |
23 | @Inject
24 | lateinit var preferences: SharedPreferences
25 |
26 | @Before
27 | fun init() {
28 | hiltRule.inject()
29 | }
30 |
31 | @Test
32 | fun savingSecureDataPrefs() {
33 | val uid = UUID.randomUUID().toString()
34 | prefs.uid = uid
35 |
36 | Assert.assertTrue(preferences is EncryptedSharedPreferences)
37 | Assert.assertEquals(prefs.uid, uid)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | RootstrapAndroidBase
4 | OK
5 |
6 |
7 | Hello blank fragment
8 | mixpanel_api_key
9 |
10 |
11 | There was an unexpected error
12 | ERROR
13 |
14 |
15 | First Name
16 | Last Name
17 | Email
18 | Password
19 | Already registered? Sign in!
20 | Sign Up
21 |
22 |
23 | Sign In
24 |
25 |
26 | Hi %1$s!
27 | Sign Out
28 | Oops! Network error
29 | Profile
30 |
31 |
--------------------------------------------------------------------------------
/fastlane/README.md:
--------------------------------------------------------------------------------
1 | # Android Fastlane configuration
2 | ============================
3 |
4 | ## Installation and requirements
5 |
6 | * Ensure JDK 1.8 is installed
7 |
8 | * Ensure proper version of Android SDK command line tools is installed
9 |
10 | * Install _fastlane_ using
11 | ```
12 | [sudo] gem install fastlane -NV
13 | ```
14 | or alternatively using `brew cask install fastlane`
15 |
16 |
17 | ## General workflow
18 |
19 | * Fastlane for Android basically executes Gradle commands for cleaning, installing Android dependencies and assembling the project into a .apk
20 | * Application file is published to Google Play Store - keystore file needs to be present under `./app` and json API key file present in the root folder.
21 |
22 |
23 | ## Actions breakdown
24 |
25 | Modify the Fastfile as appropiate for your project.
26 |
27 | Execute with
28 | ```
29 | fastlane lane_name
30 | ```
31 |
32 | ### debug_*
33 | Builds and archive corresponding flavor for local use
34 |
35 | ### deploy_*
36 | Builds corresponding flavor and pushes to Play Store
37 |
38 | ----
39 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
40 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/metrics/base/Provider.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.metrics.base
2 |
3 | import android.os.Bundle
4 | import com.google.gson.Gson
5 | import org.json.JSONException
6 | import org.json.JSONObject
7 |
8 | interface Provider {
9 |
10 | fun track(event: TrackEvent)
11 |
12 | fun reset()
13 |
14 | fun identifyUser()
15 |
16 | fun addOrEditUserSuperProperty(userProperty: UserProperty)
17 | }
18 |
19 | class UserProperty(val propertyName: String, val propertyValue: String) {
20 | fun toJsonObject(): JSONObject {
21 | return JSONObject(Gson().toJson(this))
22 | }
23 | }
24 |
25 | class TrackEvent(val eventName: String, val eventData: Any? = null) {
26 | fun actionDataToJsonObject(): JSONObject {
27 | return JSONObject(Gson().toJson(eventData))
28 | }
29 |
30 | @Throws(JSONException::class)
31 | fun actionDataToBundle(): Bundle {
32 | val jsonObject = actionDataToJsonObject()
33 | val bundle = Bundle()
34 | val iterator = jsonObject.keys()
35 |
36 | while (iterator.hasNext()) {
37 | val key = iterator.next() as String
38 | val value = jsonObject.getString(key)
39 | bundle.putString(key, value)
40 | }
41 |
42 | return bundle
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/LoadingDialogUtil.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util
2 |
3 | import android.app.AlertDialog
4 | import android.content.Context
5 | import com.rootstrap.android.R
6 | import com.rootstrap.android.ui.custom.LoadingDialog
7 |
8 | object LoadingDialogUtil {
9 |
10 | private var loadingDialog: LoadingDialog? = null
11 |
12 | fun showProgress(context: Context) {
13 | if (loadingDialog == null) {
14 | loadingDialog = LoadingDialog(context, null)
15 | }
16 |
17 | loadingDialog?.show()
18 | }
19 |
20 | fun hideProgress() {
21 | loadingDialog?.run { dismiss() }
22 | }
23 |
24 | fun showError(message: String?, context: Context) {
25 | val builder = AlertDialog.Builder(context)
26 | with(builder) {
27 | setTitle(context.getString(R.string.error))
28 |
29 | val showMessage = if (message.isNullOrEmpty())
30 | context.getString(R.string.generic_error)
31 | else message
32 |
33 | setMessage(showMessage)
34 |
35 | setPositiveButton(context.getString(R.string.ok)) { dialog, _ ->
36 | dialog.cancel()
37 | }
38 | val dialog: AlertDialog = create()
39 | dialog.show()
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/permissions/PermissionManager.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.permissions
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.content.pm.PackageManager
6 | import android.net.Uri
7 | import android.os.Build
8 | import android.provider.Settings
9 | import androidx.core.content.ContextCompat.checkSelfPermission
10 |
11 | interface PermissionResponse {
12 | fun granted()
13 | fun denied()
14 | fun foreverDenied()
15 | }
16 |
17 | val REQUEST_PERMISSION_REQUEST_CODE = 999
18 |
19 | fun Context.checkPermission(permission: String): Boolean =
20 | Build.VERSION.SDK_INT < Build.VERSION_CODES.M || checkSelfPermission(
21 | this,
22 | permission
23 | ) == PackageManager.PERMISSION_GRANTED
24 |
25 | fun Context.checkNotGrantedPermissions(permissions: Array): List =
26 | permissions.filter { !checkPermission(it) }
27 |
28 | /**
29 | * Use this extension to open the app details to grant permission manually
30 | * in case that the user denied the permission all the time
31 | * **/
32 | fun Context.openAppSettings() =
33 | startActivity(
34 | Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).also {
35 | it.data = Uri.parse("package:" + this.packageName)
36 | }
37 | )
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/metrics/Analytics.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.metrics
2 |
3 | import com.rootstrap.android.metrics.base.BaseAnalytics
4 | import com.rootstrap.android.metrics.base.Provider
5 | import com.rootstrap.android.metrics.base.TrackEvent
6 | import com.rootstrap.android.metrics.base.UserProperty
7 |
8 | object Analytics : BaseAnalytics {
9 | var providers: ArrayList = ArrayList()
10 |
11 | override fun addProviders(providers: ArrayList) {
12 | this.providers = providers
13 | }
14 |
15 | override fun addProvider(provider: Provider) {
16 | provider.let {
17 | providers.add(it)
18 | it.identifyUser()
19 | }
20 | }
21 |
22 | override fun addOrEditProperty(property: UserProperty) {
23 | providers.forEach { it.addOrEditUserSuperProperty(property) }
24 | }
25 |
26 | override fun addOrEditProperties(properties: List) {
27 | providers.forEach {
28 | properties.forEach { property ->
29 | it.addOrEditUserSuperProperty(property)
30 | }
31 | }
32 | }
33 |
34 | override fun identifyUser() {
35 | providers.forEach { it.identifyUser() }
36 | }
37 |
38 | override fun track(event: TrackEvent) {
39 | providers.forEach { it.track(event) }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/metrics/Events.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.metrics
2 |
3 | import com.rootstrap.android.metrics.base.TrackEvent
4 |
5 | class UserEvents {
6 | companion object {
7 | /**
8 | * Analytics login event
9 | * @param data [Any] Optional
10 | * **/
11 | fun login(data: Any? = null): TrackEvent {
12 | // You can do something before create the event for example
13 | // create a super property with the current user info
14 | // appAnalytics.addOrEditProperty(UserProperty("user_name", "Fulano"))
15 | // the same for the other events
16 | Analytics.identifyUser()
17 | return TrackEvent(LOGIN, data)
18 | }
19 |
20 | /**
21 | * Analytics logout event
22 | * @param data [Any] Optional
23 | * **/
24 | fun logout(data: Any? = null) = TrackEvent(LOGOUT, data)
25 |
26 | /**
27 | * Analytics signup event
28 | * @param data [Any] Optional
29 | * **/
30 | fun signup(data: Any? = null) = TrackEvent(SIGNUP, data)
31 | }
32 | }
33 |
34 | class PageEvents {
35 | companion object {
36 | /**
37 | * Analytics visit page event
38 | * @param data [Any] Optional
39 | * **/
40 | fun visit(page: String, data: Any? = null) = TrackEvent(page, data)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/services/ResponseInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.services
2 |
3 | import com.rootstrap.android.network.managers.session.SessionManager
4 | import com.rootstrap.android.util.Prefs
5 | import okhttp3.Interceptor
6 | import okhttp3.Response
7 | import java.io.IOException
8 | import javax.inject.Inject
9 |
10 | class ResponseInterceptor @Inject constructor(
11 | private val prefs: Prefs,
12 | private val sessionManager: SessionManager
13 | ) : Interceptor {
14 |
15 | @Throws(IOException::class)
16 | override fun intercept(chain: Interceptor.Chain): Response {
17 | val response = chain.proceed(chain.request())
18 | prefer(response)
19 | return response
20 | }
21 |
22 | private fun prefer(response: Response) {
23 | val accessToken = response.header(prefs.ACCESS_TOKEN)
24 | val client = response.header(prefs.CLIENT)
25 | val uid = response.header(prefs.UID)
26 | if (preferValid(accessToken, client, uid))
27 | sessionManager.addAuthenticationHeaders(accessToken!!, client!!, uid!!)
28 | }
29 |
30 | private fun preferValid(accessToken: String?, client: String?, uid: String?): Boolean {
31 | return accessToken != null && !accessToken.isEmpty() &&
32 | client != null && !client.isEmpty() &&
33 | uid != null && !uid.isEmpty()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/rootstrap/android/utils/MockServer.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.utils
2 |
3 | import okhttp3.mockwebserver.MockResponse
4 | import okhttp3.mockwebserver.MockWebServer
5 | import java.io.IOException
6 |
7 | object MockServer {
8 |
9 | private val MOCK_WEB_SERVER_PORT = 8000
10 |
11 | private var mockServer = MockWebServer()
12 |
13 | fun stopServer() {
14 | try {
15 | mockServer.shutdown()
16 | mockServer = MockWebServer()
17 | } catch (ignored: IOException) {
18 | }
19 | }
20 |
21 | fun startServer() {
22 | try {
23 | mockServer.start(MOCK_WEB_SERVER_PORT)
24 | } catch (ignored: IOException) {
25 | }
26 | }
27 |
28 | fun server(): MockWebServer {
29 | return mockServer
30 | }
31 |
32 | fun successfulResponse() = MockResponse()
33 | .setResponseCode(200)
34 | .addHeader("Content-Type", "application/json; charset=utf-8")
35 | .addHeader("Connection", "close")
36 | .addHeader("Cache-Control", "no-cache")
37 | .setBody("{ }")
38 |
39 | fun notFoundResponse() = MockResponse()
40 | .addHeader("Connection", "close")
41 | .setResponseCode(404)
42 | .setBody("{ }")
43 |
44 | fun unauthorizedResponse() = MockResponse()
45 | .addHeader("Connection", "close")
46 | .setResponseCode(401)
47 | .setBody("{ }")
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/ErrorUtil.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util
2 |
3 | import com.google.android.material.textfield.TextInputLayout
4 | import com.rootstrap.android.network.models.ErrorModel
5 |
6 | class ErrorUtil {
7 |
8 | companion object {
9 | fun handleCustomError(error: ErrorModel): String {
10 | var message = ""
11 | if (error.errors != null) {
12 | if (error.errors is List<*> && !error.errors.isEmpty()) {
13 | if (error.errors.first() is String) {
14 | message = error.errors.first() as String
15 | }
16 | } else if (error.errors is Map<*, *> && error.errors.keys.first() is String &&
17 | error.errors.values.first() is List<*>
18 | ) {
19 | val errors = error.errors as Map>
20 | message = errors.getValue("full_messages").first()
21 | }
22 | } else if (error.error != null && !error.error.isEmpty()) {
23 | message = error.error
24 | }
25 |
26 | return message
27 | }
28 |
29 | fun displayError(inputLayout: TextInputLayout, message: String) {
30 | inputLayout.isErrorEnabled = true
31 | inputLayout.error = message
32 | }
33 | }
34 |
35 | class ErrorsEvent(val error: String)
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/custom/CustomTypefaceSpan.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.custom
2 |
3 | import android.graphics.Paint
4 | import android.graphics.Typeface
5 | import android.text.TextPaint
6 | import android.text.style.TypefaceSpan
7 |
8 | /**
9 | * Allows to apply a custom font in an spannableString
10 | *
11 | * Taken from https://stackoverflow.com/questions/10675070/multiple-typeface-in-single-textview
12 | * with slightly modifications
13 | */
14 | class CustomTypefaceSpan(private val newType: Typeface) : TypefaceSpan("") {
15 |
16 | override fun updateDrawState(ds: TextPaint) {
17 | applyCustomTypeFace(ds, newType)
18 | }
19 |
20 | override fun updateMeasureState(paint: TextPaint) {
21 | applyCustomTypeFace(paint, newType)
22 | }
23 |
24 | companion object {
25 |
26 | private const val ITALIC_SKEW_FACTOR = -0.25F
27 |
28 | private fun applyCustomTypeFace(paint: Paint, tf: Typeface) {
29 | val oldStyle: Int
30 | val old = paint.typeface
31 | oldStyle = old?.style ?: 0
32 | val fake = oldStyle and tf.style.inv()
33 | if (fake and Typeface.BOLD != 0) {
34 | paint.isFakeBoldText = true
35 | }
36 | if (fake and Typeface.ITALIC != 0) {
37 | paint.textSkewX = ITALIC_SKEW_FACTOR
38 | }
39 | paint.typeface = tf
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/Prefs.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util
2 |
3 | import android.content.SharedPreferences
4 | import com.google.gson.Gson
5 | import com.rootstrap.android.network.models.User
6 | import com.rootstrap.android.util.extensions.fromJson
7 | import javax.inject.Inject
8 |
9 | class Prefs @Inject constructor(private val prefs: SharedPreferences) {
10 |
11 | val ACCESS_TOKEN = "access-token"
12 | val CLIENT = "Client"
13 | val UID = "uid"
14 | val USER = "user"
15 | val SIGNED_IN = "signed_in"
16 |
17 | private val gson: Gson = Gson()
18 |
19 | var accessToken: String
20 | get() = prefs.getString(ACCESS_TOKEN, "")!!
21 | set(value) = prefs.edit().putString(ACCESS_TOKEN, value).apply()
22 |
23 | var client: String
24 | get() = prefs.getString(CLIENT, "")!!
25 | set(value) = prefs.edit().putString(CLIENT, value).apply()
26 |
27 | var uid: String
28 | get() = prefs.getString(UID, "")!!
29 | set(value) = prefs.edit().putString(UID, value).apply()
30 |
31 | var user: User?
32 | get() = gson.fromJson(prefs.getString(USER, "")!!)
33 | set(value) = prefs.edit().putString(USER, gson.toJson(value)).apply()
34 |
35 | var signedIn: Boolean
36 | get() = prefs.getBoolean(SIGNED_IN, false)
37 | set(value) = prefs.edit().putBoolean(SIGNED_IN, value).apply()
38 |
39 | fun clear() = prefs.edit().clear().apply()
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_profile.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
17 |
18 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_onboarding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
14 |
15 |
19 |
20 |
21 |
22 |
27 |
28 |
33 |
34 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 | # Disable incremental processing because airbnb deeplink 4.1.0 doesn't support it
23 | # Should enable when it is supported
24 | kapt.incremental.apt=false
25 |
26 | ##Signing configurations
27 | projectKeyAlias = PAlias
28 | projectKeyPassword = KeyPassword
29 | projectStoreFile = KeyStorePathFile
30 | projectStorePassword = KeyStorePassword
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/base/BaseNavActivity.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.base
2 |
3 | import android.os.Bundle
4 | import androidx.fragment.app.Fragment
5 | import androidx.navigation.NavController
6 | import androidx.navigation.findNavController
7 | import androidx.navigation.fragment.NavHostFragment
8 | import com.rootstrap.android.R
9 |
10 | open class BaseNavActivity : BaseActivity() {
11 |
12 | open var navGraph: Int? = null
13 | private lateinit var navController: NavController
14 | private val currentFragment: Fragment
15 | get() = supportFragmentManager.findFragmentById(R.id.fragment_container)?.childFragmentManager!!.fragments[0]
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | setContentView(R.layout.activity_base_nav)
20 | navGraph?.let { navigation ->
21 | navController = findNavController(R.id.fragment_container).also {
22 | it.setGraph(navigation)
23 | }
24 | }
25 | }
26 |
27 | fun navigateTo(routeOrAction: Int, bundle: Bundle? = null) {
28 | navController.navigate(routeOrAction, bundle)
29 | }
30 |
31 | override fun onBackPressed() {
32 | navigateBackFromFragment(currentFragment)
33 | }
34 |
35 | private fun navigateBackFromFragment(fragment: Fragment, toDestination: Int? = null) {
36 | toDestination?.let {
37 | NavHostFragment.findNavController(fragment).popBackStack(it, false)
38 | } ?: run {
39 | if (!NavHostFragment.findNavController(fragment).popBackStack()) {
40 | super.onBackPressed()
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # Uncomment the line if you want fastlane to automatically update itself
2 | # update_fastlane
3 |
4 | default_platform(:android)
5 |
6 | skip_docs
7 |
8 | platform :android do
9 | lane :deploy_production do
10 | release(flavor: 'Prod', track: 'internal')
11 | end
12 |
13 | lane :deploy_dev do
14 | release(flavor: 'Dev', track: 'internal')
15 | end
16 |
17 | lane :deploy_staging do
18 | release(flavor: 'Staging', track: 'internal')
19 | end
20 |
21 | lane :debug_production do
22 | debug(flavor: 'Prod')
23 | end
24 |
25 | lane :debug_dev do
26 | debug(flavor: 'Dev')
27 | end
28 |
29 | lane :debug_staging do
30 | debug(flavor: 'Staging')
31 | end
32 |
33 | lane :release do |options|
34 | increment_version_code(
35 | gradle_file_path: “app/build.gradle”
36 | )
37 |
38 | gradle(
39 | task: "clean"
40 | )
41 |
42 | gradle(
43 | task: 'androidDependencies',
44 | print_command: false
45 | )
46 |
47 | build_android_app(
48 | task: 'assemble',
49 | flavor: options[:flavor],
50 | build_type: 'Release',
51 | print_command: false,
52 | )
53 |
54 | upload_to_play_store(
55 | track: options[:track]
56 | )
57 | end
58 |
59 | lane :debug do |options|
60 | increment_version_code(
61 | gradle_file_path: “app/build.gradle”
62 | )
63 |
64 | gradle(
65 | task: "clean"
66 | )
67 |
68 | gradle(
69 | task: 'androidDependencies',
70 | print_command: false
71 | )
72 |
73 | build_android_app(
74 | task: 'assemble',
75 | flavor: options[:flavor],
76 | build_type: 'Debug',
77 | print_command: false,
78 | )
79 |
80 | # Do something with the apk
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/extensions/Fragment.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.extensions
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.lifecycle.Lifecycle
5 | import androidx.lifecycle.lifecycleScope
6 | import androidx.lifecycle.repeatOnLifecycle
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.launch
9 |
10 | /**
11 | * Allows the collection of an unlimited amount of flows in an inline style without needing to
12 | * add boilerplate indentations.
13 | *
14 | * The flows are all collected inside the same lifecycleState but it can be customized. By default
15 | * it is Lifecycle.State.STARTED.
16 | *
17 | * This extension can be called as many times as needed without problems,
18 | * but generally you will only require to call it once thanks to the vararg operator.
19 | *
20 | * If called with a single Flow Pair the type of the flow is inferred automatically, otherwise
21 | * you will need to cast the flow values when needed.
22 | *
23 | * Example of use :
24 | * - Single flow collection :
25 | * > `collectOnLifeCycle(Pair(viewModel.eventsFlow) { it.collect { event -> onEvents(event) } })`
26 | * - Multi flow collection :
27 | * > Just appending a trailing "," and adding a new Pair allows you to collect an additional
28 | * flow per pair
29 | */
30 | fun Fragment.collectOnLifeCycle(
31 | vararg flowPairs: Pair, suspend (Flow) -> Unit>,
32 | lifecycleState: Lifecycle.State = Lifecycle.State.STARTED
33 | ) {
34 | lifecycleScope.launch {
35 | repeatOnLifecycle(lifecycleState) {
36 | flowPairs.forEach {
37 | launch {
38 | it.second(it.first)
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/extensions/ActionCallback.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.extensions
2 |
3 | import com.google.gson.Gson
4 | import com.rootstrap.android.network.models.ErrorModel
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.withContext
7 | import retrofit2.Call
8 | import retrofit2.Response
9 |
10 | class ActionCallback {
11 |
12 | companion object {
13 |
14 | suspend fun call(apiCall: Call): Result> =
15 | withContext(Dispatchers.IO) {
16 | val response = apiCall.execute()
17 | handleResponse(response)
18 | }
19 |
20 | private fun handleResponse(response: Response): Result> {
21 | if (response.isSuccessful) {
22 | return Result.success(
23 | Data(response.body())
24 | )
25 | } else {
26 | try {
27 | response.errorBody()?.let {
28 | val apiError = Gson().fromJson(it.charStream(), ErrorModel::class.java)
29 | return Result.failure(
30 | ApiException(
31 | errorMessage = apiError.error
32 | )
33 | )
34 | }
35 | } catch (ignore: Exception) {
36 | }
37 | }
38 |
39 | return Result.failure(ApiException(errorType = ApiErrorType.unknownError))
40 | }
41 | }
42 | }
43 |
44 | class Data(val value: T?)
45 |
46 | class ApiException(
47 | private val errorMessage: String? = null,
48 | val errorType: ApiErrorType = ApiErrorType.apiError
49 | ) : java.lang.Exception() {
50 | override val message: String?
51 | get() = errorMessage
52 | }
53 |
54 | enum class ApiErrorType {
55 | apiError,
56 | unknownError
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/viewmodel/ProfileViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.viewmodel
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.viewModelScope
6 | import com.rootstrap.android.network.managers.session.SessionManager
7 | import com.rootstrap.android.network.managers.user.UserManager
8 | import com.rootstrap.android.ui.base.BaseViewModel
9 | import com.rootstrap.android.util.NetworkState
10 | import com.rootstrap.android.util.extensions.ApiErrorType
11 | import com.rootstrap.android.util.extensions.ApiException
12 | import dagger.hilt.android.lifecycle.HiltViewModel
13 | import kotlinx.coroutines.launch
14 | import javax.inject.Inject
15 |
16 | @HiltViewModel
17 | open class ProfileViewModel @Inject constructor(
18 | private val sessionManager: SessionManager,
19 | private val userManager: UserManager
20 | ) : BaseViewModel() {
21 |
22 | private val _state = MutableLiveData()
23 | val state: LiveData
24 | get() = _state
25 |
26 | fun signOut() {
27 | _networkState.value = NetworkState.loading
28 | viewModelScope.launch {
29 | val result = userManager.signOut()
30 | if (result.isSuccess) {
31 | _networkState.value = NetworkState.idle
32 | _state.value = ProfileState.signOutSuccess
33 | sessionManager.signOut()
34 | } else {
35 | handleError(result.exceptionOrNull())
36 | }
37 | }
38 | }
39 |
40 | private fun handleError(exception: Throwable?) {
41 | error = if (exception is ApiException && exception.errorType == ApiErrorType.apiError) {
42 | exception.message
43 | } else null
44 |
45 | _networkState.value = NetworkState.idle
46 | _networkState.value = NetworkState.error
47 | _state.value = ProfileState.signOutFailure
48 | }
49 | }
50 |
51 | enum class ProfileState {
52 | signOutFailure,
53 | signOutSuccess
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.base
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import androidx.fragment.app.Fragment
6 | import com.rootstrap.android.R
7 | import com.rootstrap.android.util.LoadingDialogUtil
8 | import com.rootstrap.android.util.NetworkState
9 | import dagger.hilt.android.AndroidEntryPoint
10 |
11 | @AndroidEntryPoint
12 | open class BaseFragment : Fragment(), BaseView {
13 |
14 | override fun showProgress() {
15 | LoadingDialogUtil.showProgress(requireContext())
16 | }
17 |
18 | override fun hideProgress() {
19 | LoadingDialogUtil.hideProgress()
20 | }
21 |
22 | override fun showError(message: String?) {
23 | LoadingDialogUtil.showError(message, requireContext())
24 | }
25 |
26 | fun openActivity(activity: Class<*>, clearTask: Boolean = false, extras: Bundle? = null) {
27 | requireActivity().startActivity(
28 | Intent(requireActivity(), activity).also {
29 | if (clearTask) {
30 | it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
31 | it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
32 | }
33 | extras?.let { bundle -> it.putExtras(bundle) }
34 | }
35 | )
36 | if (clearTask) {
37 | requireActivity().finish()
38 | }
39 | }
40 |
41 | fun observeNetwork(baseViewModel: BaseViewModel) {
42 | baseViewModel.networkState.observe(this, {
43 | when (it) {
44 | NetworkState.loading -> showProgress()
45 | NetworkState.idle -> hideProgress()
46 | else -> showError(baseViewModel.error ?: getString(R.string.default_error))
47 | }
48 | })
49 | }
50 |
51 | fun navigateTo(routeOrAction: Int, bundle: Bundle? = null) {
52 | requireActivity().takeIf { it is BaseNavActivity }?.let {
53 | it as BaseNavActivity
54 | it.navigateTo(routeOrAction, bundle)
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/metrics/GoogleAnalytics.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.metrics
2 |
3 | import android.content.Context
4 | import com.google.firebase.analytics.FirebaseAnalytics
5 | import com.rootstrap.android.metrics.base.Provider
6 | import com.rootstrap.android.metrics.base.TrackEvent
7 | import com.rootstrap.android.metrics.base.UserProperty
8 | import org.json.JSONException
9 |
10 | /**
11 | * Reference: https://firebase.google.com/docs/analytics/android/start
12 | * */
13 | class GoogleAnalytics(context: Context) : Provider {
14 | var analytic: FirebaseAnalytics = FirebaseAnalytics.getInstance(context)
15 |
16 | /**
17 | * Track any event in the app
18 | * @param event App Event to track
19 | * */
20 | override fun track(event: TrackEvent) {
21 | analytic.let {
22 | if (event.eventName.isNotEmpty()) {
23 | event.eventData?.let { _ ->
24 | try {
25 | it.logEvent(event.eventName, event.actionDataToBundle())
26 | } catch (e: JSONException) {
27 | it.logEvent(event.eventName, null)
28 | e.printStackTrace()
29 | }
30 | } ?: it.logEvent(event.eventName, null)
31 | }
32 | }
33 | }
34 |
35 | /**
36 | * Reset analytic
37 | */
38 | override fun reset() {
39 | analytic.resetAnalyticsData()
40 | }
41 |
42 | /**
43 | * Initialize google analytics user and default data
44 | * analytic.setUserId(userId)
45 | * */
46 | override fun identifyUser() {
47 | // TODO see the comments ↑↑↑
48 | }
49 |
50 | /**
51 | * @param userProperty
52 | * Sample
53 | * UserProperty("Type","Admin")
54 | * UserProperty("Type","Customer")
55 | * UserProperty("Type","Anonymous")
56 | * https://firebase.google.com/docs/analytics/android/properties
57 | * */
58 | override fun addOrEditUserSuperProperty(userProperty: UserProperty) {
59 | analytic.setUserProperty(userProperty.propertyName, userProperty.propertyValue)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/UtilModule.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import android.security.keystore.KeyGenParameterSpec
6 | import android.security.keystore.KeyProperties
7 | import androidx.security.crypto.EncryptedSharedPreferences
8 | import androidx.security.crypto.MasterKey
9 | import com.rootstrap.android.BuildConfig
10 | import com.rootstrap.android.util.dispatcher.AppDispatcherProvider
11 | import com.rootstrap.android.util.dispatcher.DispatcherProvider
12 | import com.squareup.otto.Bus
13 | import dagger.Module
14 | import dagger.Provides
15 | import dagger.hilt.InstallIn
16 | import dagger.hilt.android.qualifiers.ApplicationContext
17 | import dagger.hilt.components.SingletonComponent
18 | import javax.inject.Singleton
19 |
20 | @Module
21 | @InstallIn(SingletonComponent::class)
22 | class UtilModule {
23 |
24 | @Provides
25 | @Singleton
26 | fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
27 |
28 | val spec = KeyGenParameterSpec.Builder(
29 | BuildConfig.SECURE_KEY_ALIAS,
30 | KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
31 | )
32 | .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
33 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
34 | .setKeySize(256)
35 | .build()
36 |
37 | val masterKey = MasterKey.Builder(context, BuildConfig.SECURE_KEY_ALIAS)
38 | .setKeyGenParameterSpec(spec)
39 | .build()
40 |
41 | return EncryptedSharedPreferences.create(
42 | context,
43 | BuildConfig.SECURE_FILE_NAME,
44 | masterKey,
45 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
46 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
47 | )
48 | }
49 |
50 | @Provides
51 | @Singleton
52 | fun provideBus(): Bus = Bus()
53 |
54 | @Provides
55 | @Singleton
56 | fun provideDispatcher(): DispatcherProvider = AppDispatcherProvider()
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/permissions/PermissionActivity.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.permissions
2 |
3 | import android.content.pm.PackageManager
4 | import androidx.core.app.ActivityCompat
5 | import com.rootstrap.android.ui.base.BaseActivity
6 |
7 | open class PermissionActivity : BaseActivity() {
8 |
9 | private var permissionListener: PermissionResponse? = null
10 |
11 | fun requestPermission(permissions: Array, listener: PermissionResponse) {
12 | permissionListener = listener
13 | val notGrantedPermissions = this.checkNotGrantedPermissions(permissions)
14 |
15 | when {
16 | notGrantedPermissions.isEmpty() -> permissionListener?.granted()
17 | else -> ActivityCompat.requestPermissions(
18 | this,
19 | notGrantedPermissions.toTypedArray(),
20 | REQUEST_PERMISSION_REQUEST_CODE
21 | )
22 | }
23 | }
24 |
25 | override fun onRequestPermissionsResult(
26 | requestCode: Int,
27 | permissions: Array,
28 | grantResults: IntArray
29 | ) {
30 | permissionListener?.let { listener ->
31 | if (requestCode != REQUEST_PERMISSION_REQUEST_CODE) {
32 | return
33 | }
34 |
35 | val deniedPermissions = mutableListOf()
36 |
37 | for (i in grantResults.indices) {
38 | if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
39 | deniedPermissions.add(permissions[i])
40 | }
41 | }
42 |
43 | when {
44 | deniedPermissions.isEmpty() -> listener.granted()
45 | else -> {
46 | for (deniedPermission in deniedPermissions) {
47 | if (!ActivityCompat.shouldShowRequestPermissionRationale(this, deniedPermission)) {
48 | listener.foreverDenied()
49 | return
50 | }
51 | }
52 | listener.denied()
53 | }
54 | }
55 | }
56 |
57 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/network/providers/ServiceProviderModule.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.network.providers
2 |
3 | import com.rootstrap.android.BuildConfig
4 | import com.rootstrap.android.network.services.AuthenticationInterceptor
5 | import com.rootstrap.android.network.services.HeadersInterceptor
6 | import com.rootstrap.android.network.services.ResponseInterceptor
7 | import com.squareup.moshi.Moshi
8 | import dagger.Module
9 | import dagger.Provides
10 | import dagger.hilt.InstallIn
11 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
12 | import dagger.hilt.components.SingletonComponent
13 | import okhttp3.OkHttpClient
14 | import okhttp3.logging.HttpLoggingInterceptor
15 | import retrofit2.Retrofit
16 | import retrofit2.converter.moshi.MoshiConverterFactory
17 | import javax.inject.Singleton
18 |
19 | @Module
20 | @InstallIn(SingletonComponent::class)
21 | class ServiceProviderModule {
22 |
23 | @Provides
24 | @Singleton
25 | fun provideOkHttpClient(
26 | authenticationInterceptor: AuthenticationInterceptor,
27 | responseInterceptor: ResponseInterceptor
28 | ): OkHttpClient {
29 | val httpInterceptorLevel = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
30 | else HttpLoggingInterceptor.Level.BASIC
31 |
32 | return OkHttpClient.Builder()
33 | .addInterceptor(HeadersInterceptor())
34 | .addInterceptor(authenticationInterceptor)
35 | .addInterceptor(responseInterceptor)
36 | .addInterceptor(HttpLoggingInterceptor().setLevel(httpInterceptorLevel))
37 | .build()
38 | }
39 |
40 | @Provides
41 | @Singleton
42 | fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
43 | val url = URL_API ?: BuildConfig.API_URL
44 | val moshi = Moshi.Builder()
45 | .add(KotlinJsonAdapterFactory())
46 | .build()
47 | return Retrofit.Builder()
48 | .baseUrl(url)
49 | .addConverterFactory(MoshiConverterFactory.create(moshi).withNullSerialization())
50 | .client(okHttpClient)
51 | .build()
52 | }
53 |
54 | companion object {
55 | var URL_API: String? = null
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/permissions/PermissionFragment.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.permissions
2 |
3 | import android.content.pm.PackageManager
4 | import com.rootstrap.android.ui.base.BaseFragment
5 |
6 | open class PermissionFragment : BaseFragment() {
7 |
8 | private var permissionListener: PermissionResponse? = null
9 |
10 | private fun requestPermission(permissions: Array, listener: PermissionResponse) {
11 | permissionListener = listener
12 | activity?.let { activityContext ->
13 | val notGrantedPermissions = activityContext.checkNotGrantedPermissions(permissions)
14 |
15 | when {
16 | notGrantedPermissions.isEmpty() -> permissionListener?.granted()
17 | else -> requestPermissions(
18 | notGrantedPermissions.toTypedArray(),
19 | REQUEST_PERMISSION_REQUEST_CODE
20 | )
21 | }
22 | }
23 | }
24 |
25 | override fun onRequestPermissionsResult(
26 | requestCode: Int,
27 | permissions: Array,
28 | grantResults: IntArray
29 | ) {
30 | permissionListener?.let { listener ->
31 | if (requestCode != REQUEST_PERMISSION_REQUEST_CODE) {
32 | return
33 | }
34 |
35 | val deniedPermissions = mutableListOf()
36 |
37 | for (i in grantResults.indices) {
38 | if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
39 | deniedPermissions.add(permissions[i])
40 | }
41 | }
42 |
43 | when {
44 | deniedPermissions.isEmpty() -> listener.granted()
45 | else -> {
46 | for (deniedPermission in deniedPermissions) {
47 | if (!shouldShowRequestPermissionRationale(deniedPermission)) {
48 | listener.foreverDenied()
49 | return
50 | }
51 | }
52 | listener.denied()
53 | }
54 | }
55 | }
56 |
57 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/viewmodel/SignInViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.viewmodel
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.viewModelScope
6 | import com.rootstrap.android.network.managers.session.SessionManager
7 | import com.rootstrap.android.network.managers.user.UserManager
8 | import com.rootstrap.android.network.models.User
9 | import com.rootstrap.android.ui.base.BaseViewModel
10 | import com.rootstrap.android.util.NetworkState
11 | import com.rootstrap.android.util.dispatcher.DispatcherProvider
12 | import com.rootstrap.android.util.extensions.ApiErrorType
13 | import com.rootstrap.android.util.extensions.ApiException
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.launch
16 | import javax.inject.Inject
17 |
18 | @HiltViewModel
19 | open class SignInViewModel @Inject constructor(
20 | private val sessionManager: SessionManager,
21 | private val userManager: UserManager,
22 | private val dispatcher: DispatcherProvider
23 | ) : BaseViewModel() {
24 |
25 | private val _state = MutableLiveData()
26 | val state: LiveData
27 | get() = _state
28 |
29 | fun signIn(user: User) {
30 | _networkState.value = NetworkState.loading
31 | // Avoid using hardcoded dispatcher this way can be mocked later
32 | viewModelScope.launch(dispatcher.io) {
33 | val result = userManager.signIn(user = user)
34 | if (result.isSuccess) {
35 | result.getOrNull()?.value?.user?.let { user ->
36 | sessionManager.signIn(user)
37 | }
38 |
39 | _networkState.value = NetworkState.idle
40 | _state.value = SignInState.signInSuccess
41 | } else {
42 | handleError(result.exceptionOrNull())
43 | }
44 | }
45 | }
46 |
47 | private fun handleError(exception: Throwable?) {
48 | error = if (exception is ApiException && exception.errorType == ApiErrorType.apiError) {
49 | exception.message
50 | } else null
51 |
52 | _networkState.value = NetworkState.error
53 | _state.value = SignInState.signInFailure
54 | }
55 | }
56 |
57 | enum class SignInState {
58 | signInFailure,
59 | signInSuccess
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/viewmodel/SignUpViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.viewmodel
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.viewModelScope
6 | import com.rootstrap.android.network.managers.session.SessionManager
7 | import com.rootstrap.android.network.managers.user.UserManager
8 | import com.rootstrap.android.network.models.User
9 | import com.rootstrap.android.ui.base.BaseViewModel
10 | import com.rootstrap.android.util.NetworkState
11 | import com.rootstrap.android.util.dispatcher.DispatcherProvider
12 | import com.rootstrap.android.util.extensions.ApiErrorType
13 | import com.rootstrap.android.util.extensions.ApiException
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.launch
16 | import javax.inject.Inject
17 |
18 | @HiltViewModel
19 | open class SignUpViewModel @Inject constructor(
20 | private val sessionManager: SessionManager,
21 | private val userManager: UserManager,
22 | private val dispatcher: DispatcherProvider
23 | ) : BaseViewModel() {
24 |
25 | private val _state = MutableLiveData()
26 | val state: LiveData
27 | get() = _state
28 |
29 | fun signUp(user: User) {
30 | _networkState.value = NetworkState.loading
31 | // Avoid using hardcoded dispatcher this way can be mocked later
32 | viewModelScope.launch(dispatcher.io) {
33 | val result = userManager.signUp(user = user)
34 |
35 | if (result.isSuccess) {
36 | result.getOrNull()?.value?.user?.let { user ->
37 | sessionManager.signIn(user)
38 | }
39 |
40 | _networkState.value = NetworkState.idle
41 | _state.value = SignUpState.signUpSuccess
42 | } else {
43 | handleError(result.exceptionOrNull())
44 | }
45 | }
46 | }
47 |
48 | private fun handleError(exception: Throwable?) {
49 | error = if (exception is ApiException && exception.errorType == ApiErrorType.apiError) {
50 | exception.message
51 | } else null
52 |
53 | _networkState.value = NetworkState.error
54 | _state.value = SignUpState.signUpFailure
55 | }
56 | }
57 |
58 | enum class SignUpState {
59 | signUpFailure,
60 | signUpSuccess
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/metrics/MixPanelAnalytics.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.metrics
2 |
3 | import android.content.Context
4 |
5 | import com.mixpanel.android.mpmetrics.MixpanelAPI
6 | import com.rootstrap.android.R
7 | import com.rootstrap.android.metrics.base.Provider
8 | import com.rootstrap.android.metrics.base.TrackEvent
9 | import com.rootstrap.android.metrics.base.UserProperty
10 | import org.json.JSONException
11 |
12 | /**
13 | * Mix panel android reference: https://developer.mixpanel.com/docs/android
14 | * */
15 | class MixPanelAnalytics(context: Context) : Provider {
16 | var analytic: MixpanelAPI =
17 | MixpanelAPI.getInstance(context, context.getString(R.string.mixpanel_api_key))
18 |
19 | /**
20 | * Track any event in the app
21 | * @param event Action to track
22 | * */
23 | override fun track(event: TrackEvent) {
24 | analytic.let {
25 | if (event.eventName.isNotEmpty()) {
26 | event.eventData?.let { _ ->
27 | try {
28 | it.track(event.eventName, event.actionDataToJsonObject())
29 | } catch (e: JSONException) {
30 | it.track(event.eventName)
31 | e.printStackTrace()
32 | }
33 | } ?: it.track(event.eventName)
34 | }
35 | }
36 | }
37 |
38 | /**
39 | * Reset analytic
40 | */
41 | override fun reset() {
42 | analytic.reset()
43 | }
44 |
45 | /**
46 | * Initialize mixpanel user and default data
47 | * analytic.identify(user.id)
48 | * analytic.alias(user.id, null)
49 | * val people = analytic.people
50 | * people.identify(user.Id)
51 | * people.set("\$first_name", user.username)
52 | * people.set("\$email", user.email())
53 | * analytic!!.flush()
54 | * */
55 | override fun identifyUser() {
56 | // TODO see the comments ↑↑↑
57 | }
58 |
59 | /**
60 | * @param userProperty
61 | * Sample
62 | * UserProperty("Type","Admin")
63 | * UserProperty("Type","Customer")
64 | * UserProperty("Type","Anonymous")
65 | * https://help.mixpanel.com/hc/en-us/articles/360001355526
66 | * */
67 | override fun addOrEditUserSuperProperty(userProperty: UserProperty) {
68 | analytic.registerSuperProperties(userProperty.toJsonObject())
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/rootstrap/android/tests/ProfileFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.tests
2 |
3 | import androidx.test.core.app.ActivityScenario
4 | import androidx.test.espresso.Espresso.onView
5 | import androidx.test.espresso.action.ViewActions.click
6 | import androidx.test.espresso.matcher.ViewMatchers.withId
7 | import com.rootstrap.android.R
8 | import com.rootstrap.android.ui.activity.MainActivity
9 | import com.rootstrap.android.utils.BaseTests
10 | import dagger.hilt.android.testing.HiltAndroidTest
11 | import okhttp3.mockwebserver.Dispatcher
12 | import okhttp3.mockwebserver.MockResponse
13 | import okhttp3.mockwebserver.RecordedRequest
14 | import org.junit.After
15 | import org.junit.Assert.assertEquals
16 | import org.junit.Before
17 | import org.junit.Test
18 |
19 | @HiltAndroidTest
20 | class ProfileFragmentTest : BaseTests() {
21 |
22 | private lateinit var activity: MainActivity
23 | private lateinit var scenario: ActivityScenario
24 |
25 | @Before
26 | override fun before() {
27 | super.before()
28 | setServerDispatch(logoutDispatcher())
29 | sessionManager.user = testUser()
30 | scenario = ActivityScenario.launch(MainActivity::class.java)
31 | scenario.onActivity { activity -> this.activity = activity }
32 | }
33 |
34 | @Test
35 | fun profileUiTest() {
36 | stringMatches(
37 | R.id.welcome_text_view,
38 | activity.getString(R.string.welcome_message, sessionManager.user?.firstName)
39 | )
40 | onView(withId(R.id.sign_out_button)).perform(click())
41 | assertEquals(null, sessionManager.user)
42 |
43 | // Check if this activity was successful launched
44 | activity.runOnUiThread {
45 | val current = currentActivity()
46 | assertEquals(MainActivity::class.java.name, current::class.java.name)
47 | }
48 | }
49 |
50 | private fun logoutDispatcher(): Dispatcher {
51 | return object : Dispatcher() {
52 | override fun dispatch(request: RecordedRequest): MockResponse {
53 | return if (request.path!!.contains("users/sign_out"))
54 | mockServer.successfulResponse()
55 | else
56 | mockServer.notFoundResponse()
57 | }
58 | }
59 | }
60 |
61 | @After
62 | override fun after() {
63 | super.after()
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/fragment/ProfileFragment.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.fragment
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.viewModels
8 | import com.rootstrap.android.R
9 | import com.rootstrap.android.databinding.FragmentProfileBinding
10 | import com.rootstrap.android.metrics.Analytics
11 | import com.rootstrap.android.metrics.PageEvents
12 | import com.rootstrap.android.metrics.VISIT_PROFILE
13 | import com.rootstrap.android.network.managers.session.SessionManager
14 | import com.rootstrap.android.ui.activity.OnBoardingActivity
15 | import com.rootstrap.android.ui.viewmodel.ProfileViewModel
16 | import com.rootstrap.android.ui.viewmodel.ProfileState
17 | import com.rootstrap.android.ui.base.BaseFragment
18 | import dagger.hilt.android.AndroidEntryPoint
19 | import javax.inject.Inject
20 |
21 | @AndroidEntryPoint
22 | class ProfileFragment : BaseFragment() {
23 |
24 | @Inject
25 | lateinit var sessionManager: SessionManager
26 |
27 | private val viewModel: ProfileViewModel by viewModels()
28 |
29 | private val binding by lazy { FragmentProfileBinding.inflate(layoutInflater) }
30 |
31 | override fun onCreateView(
32 | inflater: LayoutInflater,
33 | container: ViewGroup?,
34 | savedInstanceState: Bundle?
35 | ): View {
36 | return binding.root
37 | }
38 |
39 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
40 | super.onViewCreated(view, savedInstanceState)
41 | Analytics.track(PageEvents.visit(VISIT_PROFILE))
42 | setUpView()
43 | setObservers()
44 | }
45 |
46 | private fun setUpView() {
47 | with(binding) {
48 | welcomeTextView.text =
49 | getString(R.string.welcome_message, sessionManager.user?.firstName)
50 | signOutButton.setOnClickListener { viewModel.signOut() }
51 | }
52 | }
53 |
54 | private fun setObservers() {
55 | with(viewModel) {
56 | lifecycle.addObserver(this)
57 | state.observe(viewLifecycleOwner, {
58 | when (it) {
59 | ProfileState.signOutFailure -> showError(error)
60 | ProfileState.signOutSuccess -> openActivity(OnBoardingActivity::class.java, true)
61 | }
62 | })
63 | observeNetwork(this)
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/fragment/SignInFragment.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.fragment
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.viewModels
8 | import com.rootstrap.android.R
9 | import com.rootstrap.android.databinding.FragmentSignInBinding
10 | import com.rootstrap.android.metrics.Analytics
11 | import com.rootstrap.android.metrics.PageEvents
12 | import com.rootstrap.android.metrics.VISIT_SIGN_IN
13 | import com.rootstrap.android.network.models.User
14 | import com.rootstrap.android.ui.viewmodel.SignInViewModel
15 | import com.rootstrap.android.ui.viewmodel.SignInState
16 | import com.rootstrap.android.ui.base.BaseFragment
17 | import com.rootstrap.android.util.extensions.value
18 | import dagger.hilt.android.AndroidEntryPoint
19 |
20 | @AndroidEntryPoint
21 | class SignInFragment : BaseFragment() {
22 |
23 | private val viewModel: SignInViewModel by viewModels()
24 |
25 | private val binding: FragmentSignInBinding by lazy {
26 | FragmentSignInBinding.inflate(layoutInflater)
27 | }
28 |
29 | override fun onCreateView(
30 | inflater: LayoutInflater,
31 | container: ViewGroup?,
32 | savedInstanceState: Bundle?
33 | ): View {
34 | return binding.root
35 | }
36 |
37 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
38 | super.onViewCreated(view, savedInstanceState)
39 | Analytics.track(PageEvents.visit(VISIT_SIGN_IN))
40 | setUpView()
41 | setObservers()
42 | }
43 |
44 | private fun setUpView() {
45 | binding.signInButton.setOnClickListener { signIn() }
46 | }
47 |
48 | private fun signIn() {
49 | with(binding) {
50 | val user = User(
51 | email = emailEditText.value(),
52 | password = passwordEditText.value()
53 | )
54 | viewModel.signIn(user)
55 | }
56 | }
57 |
58 | private fun setObservers() {
59 | with(viewModel) {
60 | lifecycle.addObserver(this)
61 | state.observe(requireActivity(), {
62 | when (it) {
63 | SignInState.signInFailure -> showError(error)
64 | SignInState.signInSuccess -> navigateTo(R.id.nav_onboarding_to_main)
65 | }
66 | })
67 | observeNetwork(this)
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/rootstrap/android/tests/SignInFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.tests
2 |
3 | import androidx.test.core.app.ActivityScenario
4 | import com.google.gson.Gson
5 | import com.rootstrap.android.R
6 | import com.rootstrap.android.network.models.UserSerializer
7 | import com.rootstrap.android.ui.activity.MainActivity
8 | import com.rootstrap.android.ui.activity.OnBoardingActivity
9 | import com.rootstrap.android.utils.BaseTests
10 | import dagger.hilt.android.testing.HiltAndroidTest
11 | import okhttp3.mockwebserver.Dispatcher
12 | import okhttp3.mockwebserver.MockResponse
13 | import okhttp3.mockwebserver.RecordedRequest
14 | import org.junit.After
15 | import org.junit.Assert.assertEquals
16 | import org.junit.Before
17 | import org.junit.Test
18 |
19 | @HiltAndroidTest
20 | class SignInFragmentTest : BaseTests() {
21 |
22 | private lateinit var activity: OnBoardingActivity
23 | private lateinit var scenario: ActivityScenario
24 |
25 | @Before
26 | override fun before() {
27 | super.before()
28 | scenario = ActivityScenario.launch(OnBoardingActivity::class.java)
29 | scenario.onActivity { activity -> this.activity = activity }
30 | scenario.recreate()
31 | scrollAndPerformClick(R.id.sign_in_text_view)
32 | }
33 |
34 | @Test
35 | fun signInSuccessfulTest() {
36 | scenario.recreate()
37 | setServerDispatch(signInDispatcher())
38 | val testUser = testUser()
39 | typeText(R.id.email_edit_text, testUser.email)
40 | typeText(R.id.password_edit_text, testUser.password)
41 | performClick(R.id.sign_in_button)
42 | val user = sessionManager.user
43 | assertEquals(user?.email, testUser.email)
44 |
45 | activity.runOnUiThread {
46 | val current = currentActivity()
47 | assertEquals(MainActivity::class.java.name, current::class.java.name)
48 | }
49 | }
50 |
51 | private fun signInDispatcher(): Dispatcher {
52 | return object : Dispatcher() {
53 | override fun dispatch(request: RecordedRequest): MockResponse {
54 | return if (request.path!!.contains("users/sign_in")) {
55 | val userResponse = UserSerializer(testUser())
56 | mockServer.successfulResponse().setBody(
57 | Gson().toJson(userResponse)
58 | )
59 | } else
60 | mockServer.notFoundResponse()
61 | }
62 | }
63 | }
64 |
65 | @After
66 | override fun after() {
67 | super.after()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/ui/fragment/SignUpFragment.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.ui.fragment
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.viewModels
8 | import com.rootstrap.android.R
9 | import com.rootstrap.android.databinding.FragmentSignUpBinding
10 | import com.rootstrap.android.metrics.Analytics
11 | import com.rootstrap.android.metrics.PageEvents
12 | import com.rootstrap.android.metrics.VISIT_SIGN_UP
13 | import com.rootstrap.android.network.models.User
14 | import com.rootstrap.android.ui.viewmodel.SignUpViewModel
15 | import com.rootstrap.android.ui.viewmodel.SignUpState
16 | import com.rootstrap.android.ui.base.BaseFragment
17 | import com.rootstrap.android.util.extensions.value
18 | import dagger.hilt.android.AndroidEntryPoint
19 |
20 | @AndroidEntryPoint
21 | class SignUpFragment : BaseFragment() {
22 |
23 | private val viewModel: SignUpViewModel by viewModels()
24 |
25 | private val binding: FragmentSignUpBinding by lazy {
26 | FragmentSignUpBinding.inflate(layoutInflater)
27 | }
28 |
29 | override fun onCreateView(
30 | inflater: LayoutInflater,
31 | container: ViewGroup?,
32 | savedInstanceState: Bundle?
33 | ): View {
34 | return binding.root
35 | }
36 |
37 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
38 | super.onViewCreated(view, savedInstanceState)
39 | Analytics.track(PageEvents.visit(VISIT_SIGN_UP))
40 | setUpView()
41 | lifecycle.addObserver(viewModel)
42 | setObservers()
43 | }
44 |
45 | private fun setUpView() {
46 | with(binding) {
47 | signUpButton.setOnClickListener { signUp() }
48 | signInTextView.setOnClickListener { navigateTo(R.id.action_nav_to_sign_in) }
49 | }
50 | }
51 |
52 | private fun signUp() {
53 | with(binding) {
54 | val user = User(
55 | email = emailEditText.value(),
56 | firstName = firstNameEditText.value(),
57 | lastName = lastNameEditText.value(),
58 | password = passwordEditText.value()
59 | )
60 | viewModel.signUp(user)
61 | }
62 | }
63 |
64 | private fun setObservers() {
65 | with(viewModel) {
66 | state.observe(requireActivity(), {
67 | when (it) {
68 | SignUpState.signUpFailure -> showError(error)
69 | SignUpState.signUpSuccess -> navigateTo(R.id.nav_onboarding_to_main)
70 | }
71 | })
72 | observeNetwork(this)
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/rootstrap/android/tests/SignUpFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.tests
2 |
3 | import androidx.test.core.app.ActivityScenario
4 | import com.google.gson.Gson
5 | import com.rootstrap.android.R
6 | import com.rootstrap.android.network.models.UserSerializer
7 | import com.rootstrap.android.ui.activity.MainActivity
8 | import com.rootstrap.android.ui.activity.OnBoardingActivity
9 | import com.rootstrap.android.utils.BaseTests
10 | import dagger.hilt.android.testing.HiltAndroidTest
11 | import okhttp3.mockwebserver.Dispatcher
12 | import okhttp3.mockwebserver.MockResponse
13 | import okhttp3.mockwebserver.RecordedRequest
14 | import org.junit.After
15 | import org.junit.Assert.assertEquals
16 | import org.junit.Before
17 | import org.junit.Test
18 |
19 | @HiltAndroidTest
20 | class SignUpFragmentTest : BaseTests() {
21 |
22 | private lateinit var activity: OnBoardingActivity
23 | private lateinit var scenario: ActivityScenario
24 |
25 | @Before
26 | override fun before() {
27 | super.before()
28 | scenario = ActivityScenario.launch(OnBoardingActivity::class.java)
29 | scenario.onActivity { activity -> this.activity = activity }
30 | scenario.recreate()
31 | }
32 |
33 | @Test
34 | fun signUpSuccessfulTest() {
35 | scenario.recreate()
36 | setServerDispatch(signUpDispatcher())
37 | val testUser = testUser()
38 | scrollAndTypeText(R.id.first_name_edit_text, testUser.firstName)
39 | scrollAndTypeText(R.id.last_name_edit_text, testUser.lastName)
40 | scrollAndTypeText(R.id.email_edit_text, testUser.email)
41 | scrollAndTypeText(R.id.password_edit_text, testUser.password)
42 | scrollAndPerformClick(R.id.sign_up_button)
43 | val user = sessionManager.user
44 | assertEquals(user?.email, testUser.email)
45 |
46 | activity.runOnUiThread {
47 | val current = currentActivity()
48 | assertEquals(MainActivity::class.java.name, current::class.java.name)
49 | }
50 | }
51 |
52 | private fun signUpDispatcher(): Dispatcher {
53 | return object : Dispatcher() {
54 | override fun dispatch(request: RecordedRequest): MockResponse {
55 | return if (request.path!!.contains("users")) {
56 | val userResponse = UserSerializer(testUser())
57 | mockServer.successfulResponse().setBody(
58 | Gson().toJson(userResponse)
59 | )
60 | } else
61 | mockServer.notFoundResponse()
62 | }
63 | }
64 | }
65 |
66 | @After
67 | override fun after() {
68 | super.after()
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/secure/Readme.md:
--------------------------------------------------------------------------------
1 | # Secret configuration files
2 |
3 | Any sensitive files should be placed in this directory. This primarily includes:
4 |
5 | * `google-api.json`: Google Play Developer API key - See [Developer API docs](https://developers.google.com/android-publisher/getting_started)
6 |
7 | * `key.keystore`: container for cryptographic keys - see [Android keystore system](https://developer.android.com/training/articles/keystore)
8 |
9 | * `gradle.properties`: Gradle properties file including keystore location, user and password
10 |
11 |
12 | **All the plaintext files here are placed in .gitignore**
13 |
14 | ## Securing config files
15 |
16 | For running the build through a CI pipeline we need to make sure all sensitive files are available to all build agents. Managing these files separately brings issues to ensure they are: a- version-controlled in a consistent manner with the code, and b- available to every build agent at build time.
17 |
18 | We suggest using [git secret](https://git-secret.io/) as a simple and secure solution for keeping these sensitive files in the repo.
19 | This needs to be installed on any machine that requires access to the sourcecode and config (developer machines and CI agents).
20 |
21 |
22 | ## First time setup
23 |
24 | * Generate gpg key -provide Real Name and Email address (to be used as USER-ID)
25 | ```
26 | gpg --gen-key
27 | ```
28 | * (Alternatively) Import existing owner public key
29 | ```
30 | gpg --import ci-public-key.gpg
31 | ```
32 | * Install git-secret
33 | ```
34 | brew install git-secret
35 | ```
36 | * Initialize git-secret on the project root folder (this createds `.gitsecret` folder)
37 | ```
38 | git secret init
39 | ```
40 | * Add access for USER-ID and any other usernames that will require access
41 | ```
42 | git secret tell {dev-user@email}
43 | git secret tell {ci-user@email}
44 | ```
45 | * Add sensitive files to secret vault
46 | ```
47 | git secret add secure/*
48 | git secret add gradle.properties
49 | ```
50 | * Encrypt
51 | ```
52 | git secret hide
53 | ```
54 | * Commit and push files (this includes `.gitsecret` folder)
55 |
56 |
57 | ## Accesing the files
58 |
59 | * Install git secret
60 | * Ensure private gpg key matching public key for the user ID that was granted access is available
61 | * Check source code
62 | * Decrypt
63 | ```
64 | git secret reveal
65 | ```
66 |
67 | ### from CI server
68 |
69 | * Store private key into and make available to project configuration as GPG_PRIVATE_KEY
70 |
71 | * Include steps in Build pipeline
72 | ```
73 | # Install git-secret
74 | brew install git-secret
75 | # Create private key file
76 | echo $GPG_PRIVATE_KEY > ./private_key.gpg
77 | # Import private key
78 | gpg --import ./private_key.gpg
79 | # Reveal secrets
80 | git secret reveal
81 |
82 | # Continue normal build pipeline
83 | ```
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_sign_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
19 |
20 |
26 |
27 |
28 |
29 |
38 |
39 |
45 |
46 |
47 |
48 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/app/google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "289773649665",
4 | "firebase_url": "https://android-base-2deb6.firebaseio.com",
5 | "project_id": "android-base-2deb6",
6 | "storage_bucket": "android-base-2deb6.appspot.com"
7 | },
8 | "client": [
9 | {
10 | "client_info": {
11 | "mobilesdk_app_id": "1:289773649665:android:df12cc6c0442a36cf1e500",
12 | "android_client_info": {
13 | "package_name": "com.rootstrap.android"
14 | }
15 | },
16 | "oauth_client": [
17 | {
18 | "client_id": "289773649665-g16indgillq57r32d2lraqpe605gc4lr.apps.googleusercontent.com",
19 | "client_type": 3
20 | }
21 | ],
22 | "api_key": [
23 | {
24 | "current_key": "AIzaSyDrO1ywMtRvy0H7fJzYhbUBmjXQqEiUBw4"
25 | }
26 | ],
27 | "services": {
28 | "appinvite_service": {
29 | "other_platform_oauth_client": [
30 | {
31 | "client_id": "289773649665-g16indgillq57r32d2lraqpe605gc4lr.apps.googleusercontent.com",
32 | "client_type": 3
33 | }
34 | ]
35 | }
36 | }
37 | },
38 | {
39 | "client_info": {
40 | "mobilesdk_app_id": "1:289773649665:android:d4cfc41fd5630d4bf1e500",
41 | "android_client_info": {
42 | "package_name": "com.rootstrap.android.dev"
43 | }
44 | },
45 | "oauth_client": [
46 | {
47 | "client_id": "289773649665-g16indgillq57r32d2lraqpe605gc4lr.apps.googleusercontent.com",
48 | "client_type": 3
49 | }
50 | ],
51 | "api_key": [
52 | {
53 | "current_key": "AIzaSyDrO1ywMtRvy0H7fJzYhbUBmjXQqEiUBw4"
54 | }
55 | ],
56 | "services": {
57 | "appinvite_service": {
58 | "other_platform_oauth_client": [
59 | {
60 | "client_id": "289773649665-g16indgillq57r32d2lraqpe605gc4lr.apps.googleusercontent.com",
61 | "client_type": 3
62 | }
63 | ]
64 | }
65 | }
66 | },
67 | {
68 | "client_info": {
69 | "mobilesdk_app_id": "1:289773649665:android:79dc18c5b1221712f1e500",
70 | "android_client_info": {
71 | "package_name": "com.rootstrap.android.staging"
72 | }
73 | },
74 | "oauth_client": [
75 | {
76 | "client_id": "289773649665-g16indgillq57r32d2lraqpe605gc4lr.apps.googleusercontent.com",
77 | "client_type": 3
78 | }
79 | ],
80 | "api_key": [
81 | {
82 | "current_key": "AIzaSyDrO1ywMtRvy0H7fJzYhbUBmjXQqEiUBw4"
83 | }
84 | ],
85 | "services": {
86 | "appinvite_service": {
87 | "other_platform_oauth_client": [
88 | {
89 | "client_id": "289773649665-g16indgillq57r32d2lraqpe605gc4lr.apps.googleusercontent.com",
90 | "client_type": 3
91 | }
92 | ]
93 | }
94 | }
95 | }
96 | ],
97 | "configuration_version": "1"
98 | }
99 |
--------------------------------------------------------------------------------
/.github/workflows/cicd.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | env:
8 | LANG: en_US.UTF-8
9 | # Notifications
10 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_URL }}
11 | SLACK_CHANNEL: '#dev-builds'
12 |
13 | jobs:
14 |
15 | ci:
16 | runs-on: ubuntu-latest
17 | timeout-minutes: 45
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v2
21 | - name: Install Fastlane and required plugins
22 | run: |
23 | sudo gem install bundler -v "$(grep -A 1 "BUNDLED WITH" Gemfile.lock | tail -n 1)"
24 | bundle install --path vendor/bundle
25 | # Runs build with Gradle
26 | - name: Build with Fastlane
27 | run: bundle exec fastlane debug_dev
28 | - name: Send notification of build result
29 | uses: 8398a7/action-slack@v3
30 | with:
31 | status: ${{ job.status }}
32 | text: '${{github.repository}} Dev build status is ${{ job.status }}'
33 | fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
34 | if: always()
35 |
36 |
37 | release:
38 | if: ${{ github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' }}
39 | runs-on: ubuntu-latest
40 | timeout-minutes: 45
41 | env:
42 | # S3
43 | FOLDER: android-base
44 | AWS_REGION: us-east-1
45 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
46 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
47 | KEYS_BUCKET: ${{ secrets.AWS_S3_KEYS_BUCKET }}
48 | # Android Release
49 | JSON_KEYFILE: google-api.json
50 | RELEASE_STORE_FILE: key.keystore
51 | RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_STORE_PASSWORD }}
52 | RELEASE_KEY_ALIAS: debug
53 | RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }}
54 | # Notifications
55 | SLACK_URL: ${{ secrets.SLACK_URL }}
56 | SLACK_CHANNEL: '#dev-builds'
57 | steps:
58 | - name: Checkout
59 | uses: actions/checkout@v2
60 | # Downloads certificate, private key and Firebase file
61 | - name: Download code signing items
62 | run: |
63 | aws s3 cp s3://$KEYS_BUCKET/$FOLDER/ . --recursive
64 | mv ./android/$RELEASE_STORE_FILE ./app/$RELEASE_STORE_FILE
65 | - name: Install Fastlane and required plugins
66 | run: |
67 | sudo gem install bundler
68 | bundle install --path vendor/bundle
69 | # Build with Gradle and submit to Play Store
70 | - name: Submit Development build with Fastlane
71 | if: ${{ github.ref == 'refs/heads/develop' }}
72 | run: bundle exec fastlane deploy_dev
73 | - name: Submit Staging build with Fastlane
74 | if: ${{ github.ref == 'refs/heads/master' }}
75 | run: bundle exec fastlane deploy_staging
76 | - name: Send notification of build result
77 | uses: 8398a7/action-slack@v3
78 | with:
79 | status: ${{ job.status }}
80 | text: '${{github.repository}} ${{github.ref}} build submission status is ${{ job.status }}'
81 | fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
82 | if: always()
83 |
84 |
--------------------------------------------------------------------------------
/app/src/test/java/com/rootstrap/android/SignUpActivityViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android
2 |
3 | import com.rootstrap.android.network.managers.session.SessionManager
4 | import com.rootstrap.android.network.managers.user.UserManager
5 | import com.rootstrap.android.network.models.User
6 | import com.rootstrap.android.network.models.UserSerializer
7 | import com.rootstrap.android.test.TestDispatcherProvider
8 | import com.rootstrap.android.test.UnitTestBase
9 | import com.rootstrap.android.ui.viewmodel.SignUpViewModel
10 | import com.rootstrap.android.ui.viewmodel.SignUpState
11 | import com.rootstrap.android.util.NetworkState
12 | import com.rootstrap.android.util.extensions.ApiException
13 | import com.rootstrap.android.util.extensions.Data
14 | import io.mockk.coEvery
15 | import io.mockk.coVerify
16 | import io.mockk.every
17 | import io.mockk.impl.annotations.MockK
18 | import io.mockk.impl.annotations.RelaxedMockK
19 | import io.mockk.verify
20 | import kotlinx.coroutines.ExperimentalCoroutinesApi
21 | import org.junit.Assert.assertEquals
22 | import org.junit.Before
23 | import org.junit.Test
24 |
25 | @ExperimentalCoroutinesApi // This annotation is required to use TestDispatcherProvider is still an experiment
26 | class SignUpActivityViewModelTest : UnitTestBase() {
27 |
28 | private lateinit var viewModel: SignUpViewModel
29 |
30 | @RelaxedMockK
31 | lateinit var sessionManager: SessionManager
32 |
33 | @RelaxedMockK
34 | lateinit var userManager: UserManager
35 |
36 | @MockK
37 | lateinit var user: User
38 |
39 | @MockK
40 | lateinit var userSerializer: UserSerializer
41 |
42 | companion object {
43 | const val ERROR_EXAMPLE_TEXT = "Time out example"
44 | }
45 |
46 | @Before
47 | override fun setup() {
48 | super.setup()
49 | every { userSerializer.user } returns user
50 | viewModel = SignUpViewModel(sessionManager, userManager, TestDispatcherProvider())
51 | }
52 |
53 | @Test
54 | fun `signUp success assert signUpSuccess and network idle`() {
55 | var state: SignUpState? = null
56 | coEvery { userManager.signUp(user = user) } returns Result.success(Data(userSerializer))
57 |
58 | viewModel.signUp(user)
59 | viewModel.state.observeForever {
60 | state = it
61 | }
62 |
63 | assertEquals(state, SignUpState.signUpSuccess)
64 | assertEquals(viewModel.networkState.value, NetworkState.idle)
65 | verify { sessionManager.signIn(user) }
66 | coVerify { userManager.signUp(user = user) }
67 | }
68 |
69 | @Test
70 | fun `signUp fail assert signUpFailure and network error`() {
71 | var state: SignUpState? = null
72 | coEvery { userManager.signUp(user = user) } returns Result.failure(
73 | ApiException(
74 | ERROR_EXAMPLE_TEXT
75 | )
76 | )
77 |
78 | viewModel.signUp(user)
79 | viewModel.state.observeForever {
80 | state = it
81 | }
82 |
83 | assertEquals(state, SignUpState.signUpFailure)
84 | assertEquals(viewModel.networkState.value, NetworkState.error)
85 | assertEquals(viewModel.error, ERROR_EXAMPLE_TEXT)
86 | coVerify { userManager.signUp(user = user) }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/app/src/test/java/com/rootstrap/android/SignInActivityViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android
2 |
3 | import com.rootstrap.android.network.managers.session.SessionManager
4 | import com.rootstrap.android.network.managers.user.UserManager
5 | import com.rootstrap.android.network.models.User
6 | import com.rootstrap.android.network.models.UserSerializer
7 | import com.rootstrap.android.test.TestDispatcherProvider
8 | import com.rootstrap.android.test.UnitTestBase
9 | import com.rootstrap.android.ui.viewmodel.SignInViewModel
10 | import com.rootstrap.android.ui.viewmodel.SignInState
11 | import com.rootstrap.android.util.NetworkState
12 | import com.rootstrap.android.util.extensions.ApiException
13 | import com.rootstrap.android.util.extensions.Data
14 | import io.mockk.coEvery
15 | import io.mockk.coVerify
16 | import io.mockk.every
17 | import io.mockk.impl.annotations.MockK
18 | import io.mockk.impl.annotations.RelaxedMockK
19 | import io.mockk.verify
20 | import kotlinx.coroutines.ExperimentalCoroutinesApi
21 | import org.junit.Assert.assertEquals
22 | import org.junit.Before
23 | import org.junit.Test
24 |
25 | @ExperimentalCoroutinesApi
26 | class SignInActivityViewModelTest : UnitTestBase() {
27 |
28 | private lateinit var viewModel: SignInViewModel
29 |
30 | @RelaxedMockK
31 | lateinit var sessionManager: SessionManager
32 |
33 | @RelaxedMockK
34 | lateinit var userManager: UserManager
35 |
36 | @MockK
37 | lateinit var user: User
38 |
39 | @MockK
40 | lateinit var userSerializer: UserSerializer
41 |
42 | companion object {
43 | const val ERROR_EXAMPLE_TEXT = "Time out example"
44 | }
45 |
46 | @Before
47 | override fun setup() {
48 | super.setup()
49 | every { userSerializer.user } returns user
50 | viewModel = SignInViewModel(sessionManager, userManager, TestDispatcherProvider())
51 | }
52 |
53 | // reading: naming standards for unit testing https://osherove.com/blog/2005/4/3/naming-standards-for-unit-tests.html
54 | @Test
55 | fun `signIn success assert signInSuccess and network idle`() {
56 | var state: SignInState? = null
57 | coEvery { userManager.signIn(user = user) } returns Result.success(Data(userSerializer))
58 |
59 | viewModel.signIn(user)
60 | viewModel.state.observeForever {
61 | state = it
62 | }
63 |
64 | assertEquals(state, SignInState.signInSuccess)
65 | assertEquals(viewModel.networkState.value, NetworkState.idle)
66 | verify { sessionManager.signIn(user) }
67 | coVerify { userManager.signIn(user = user) }
68 | }
69 |
70 | @Test
71 | fun `signIn fail assert signInFailure and network error`() {
72 | var state: SignInState? = null
73 | coEvery { userManager.signIn(user = user) } returns Result.failure(
74 | ApiException(
75 | ERROR_EXAMPLE_TEXT
76 | )
77 | )
78 |
79 | viewModel.signIn(user)
80 | viewModel.state.observeForever {
81 | state = it
82 | }
83 |
84 | assertEquals(state, SignInState.signInFailure)
85 | assertEquals(viewModel.networkState.value, NetworkState.error)
86 | assertEquals(viewModel.error, ERROR_EXAMPLE_TEXT)
87 | coVerify { userManager.signIn(user = user) }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/rootstrap/android/utils/BaseTests.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.utils
2 |
3 | import android.app.Activity
4 | import androidx.test.espresso.Espresso.onView
5 | import androidx.test.espresso.action.ViewActions.typeText
6 | import androidx.test.espresso.action.ViewActions.scrollTo
7 | import androidx.test.espresso.action.ViewActions.click
8 | import androidx.test.espresso.action.ViewActions.clearText
9 | import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
10 | import androidx.test.espresso.assertion.ViewAssertions
11 | import androidx.test.espresso.matcher.ViewMatchers
12 | import androidx.test.espresso.matcher.ViewMatchers.withId
13 | import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
14 | import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
15 | import androidx.test.runner.lifecycle.Stage
16 | import com.rootstrap.android.network.managers.session.SessionManager
17 | import com.rootstrap.android.network.models.User
18 | import com.rootstrap.android.network.providers.ServiceProviderModule
19 | import dagger.hilt.android.testing.HiltAndroidRule
20 | import okhttp3.mockwebserver.Dispatcher
21 | import org.junit.Rule
22 | import org.junit.runner.RunWith
23 | import javax.inject.Inject
24 |
25 | @RunWith(AndroidJUnit4ClassRunner::class)
26 | open class BaseTests {
27 |
28 | @Inject lateinit var sessionManager: SessionManager
29 |
30 | var mockServer: MockServer = MockServer
31 |
32 | @get:Rule
33 | var hiltRule = HiltAndroidRule(this)
34 |
35 | open fun setServerDispatch(dispatcher: Dispatcher) {
36 | mockServer.server().dispatcher = dispatcher
37 | }
38 |
39 | open fun before() {
40 | mockServer.startServer()
41 | ServiceProviderModule.URL_API = mockServer.server().url("/").toString()
42 | hiltRule.inject()
43 | }
44 |
45 | open fun after() {
46 | mockServer.stopServer()
47 | }
48 |
49 | open fun testUser() = User(
50 | "9032",
51 | "user123@mail.com",
52 | "Richard",
53 | "Richard",
54 | "99090909",
55 | "asdasdasdasda",
56 | "Richard"
57 | )
58 |
59 | open fun scrollAndTypeText(id: Int, text: String) {
60 | onView(withId(id)).perform(
61 | scrollTo(),
62 | click(),
63 | clearText(),
64 | typeText(text),
65 | closeSoftKeyboard()
66 | )
67 | }
68 |
69 | open fun typeText(id: Int, text: String) {
70 | onView(withId(id)).perform(click(), clearText(), typeText(text), closeSoftKeyboard())
71 | }
72 |
73 | open fun scrollAndPerformClick(viewId: Int) {
74 | onView(withId(viewId)).perform(scrollTo(), click())
75 | }
76 |
77 | open fun performClick(viewId: Int) {
78 | onView(withId(viewId)).perform(click())
79 | }
80 |
81 | open fun stringMatches(viewId: Int, value: String) {
82 | onView(withId(viewId)).check(
83 | ViewAssertions.matches(
84 | ViewMatchers.withText(
85 | value
86 | )
87 | )
88 | )
89 | }
90 |
91 | open fun currentActivity(): Activity {
92 | // Get the activity that currently started
93 | val activities =
94 | ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)
95 | return activities.first()
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto init
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto init
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :init
68 | @rem Get command-line arguments, handling Windows variants
69 |
70 | if not "%OS%" == "Windows_NT" goto win9xME_args
71 |
72 | :win9xME_args
73 | @rem Slurp the command line arguments.
74 | set CMD_LINE_ARGS=
75 | set _SKIP=2
76 |
77 | :win9xME_args_slurp
78 | if "x%~1" == "x" goto execute
79 |
80 | set CMD_LINE_ARGS=%*
81 |
82 | :execute
83 | @rem Setup the command line
84 |
85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
86 |
87 | @rem Execute Gradle
88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
89 |
90 | :end
91 | @rem End local scope for the variables with windows NT shell
92 | if "%ERRORLEVEL%"=="0" goto mainEnd
93 |
94 | :fail
95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
96 | rem the _cmd.exe /c_ return code!
97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
98 | exit /b 1
99 |
100 | :mainEnd
101 | if "%OS%"=="Windows_NT" endlocal
102 |
103 | :omega
104 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rootstrap/android/util/extensions/Textview.kt:
--------------------------------------------------------------------------------
1 | package com.rootstrap.android.util.extensions
2 |
3 | import android.graphics.Typeface
4 | import android.text.SpannableStringBuilder
5 | import android.text.Spanned
6 | import android.text.TextPaint
7 | import android.text.method.LinkMovementMethod
8 | import android.text.style.CharacterStyle
9 | import android.text.style.ClickableSpan
10 | import android.view.View
11 | import android.widget.TextView
12 | import androidx.annotation.ColorRes
13 | import androidx.core.content.ContextCompat
14 | import com.rootstrap.android.R
15 | import com.rootstrap.android.ui.custom.CustomTypefaceSpan
16 |
17 | /**
18 | * Sets the first occurrence of the keyword string in this TextView's text as clickable and attach the
19 | * given OnClickListener function to it.
20 | *
21 | * Additionally, it tints the keyword using the app's colorPrimary color and adds an underline to it
22 | * so it looks and behaves like an Hyperlink.
23 | *
24 | * This behavior can be customized with the keywordColor and underline params and you can also change
25 | * the keyword's typeFace with the typeFace param, allowing you to make it bold or apply
26 | * other kinds of effects.
27 | *
28 | * You can add multiple clickable keywords by just calling this method as many times as you need in
29 | * the same textview.
30 | *
31 | * The next example illustrates how to add a link to open a web browser:
32 | *```
33 | * mySuggestionText.text = "For more information go to the admin web"
34 | * mySuggestionText.setClickableKeyword("admin web") {
35 | * // Get admin url and open the admin web via an intent
36 | * }
37 | *```
38 | */
39 | fun TextView.setClickableKeyword(
40 | keyword: String,
41 | onClickListener: () -> Unit,
42 | @ColorRes keywordColor: Int = R.color.colorPrimary,
43 | underline: Boolean = true,
44 | typeFace: Typeface? = null
45 | ) {
46 | val span: SpannableStringBuilder = getTextAsSpannable()
47 |
48 | val start = text.indexOf(keyword, 0)
49 |
50 | val clickableSpan = object : ClickableSpan() {
51 | override fun onClick(widget: View) = onClickListener()
52 | override fun updateDrawState(ds: TextPaint) {
53 | ds.color = ContextCompat.getColor(context, keywordColor)
54 | ds.isUnderlineText = underline
55 | }
56 | }
57 |
58 | typeFace?.let {
59 | span.setSpan(CustomTypefaceSpan(typeFace), start, start + keyword.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
60 | }
61 | span.setSpan(clickableSpan, start, start + keyword.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
62 |
63 | if (movementMethod !is LinkMovementMethod) {
64 | movementMethod = LinkMovementMethod.getInstance()
65 | }
66 |
67 | text = span
68 | }
69 |
70 | /**
71 | * changes the color of the first occurrence of the keyword string in this TextView's text.
72 | *
73 | * It accept a color resource as a second parameter and by default it uses the app's colorPrimary color.
74 | */
75 | fun TextView.setColoredKeyword(
76 | keyword: String,
77 | @ColorRes keywordColor: Int = R.color.colorPrimary
78 | ) {
79 | val span: SpannableStringBuilder = getTextAsSpannable()
80 | val start = text.indexOf(keyword, 0)
81 | val coloredSpan = object : CharacterStyle() {
82 | override fun updateDrawState(ds: TextPaint) {
83 | ds.color = ContextCompat.getColor(context, keywordColor)
84 | }
85 | }
86 | span.setSpan(coloredSpan, start, start + keyword.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
87 | text = span
88 | }
89 |
90 | private fun TextView.getTextAsSpannable() =
91 | if (text is SpannableStringBuilder) text as SpannableStringBuilder
92 | else SpannableStringBuilder(text)
93 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.2)
5 | addressable (2.7.0)
6 | public_suffix (>= 2.0.2, < 5.0)
7 | atomos (0.1.3)
8 | aws-eventstream (1.1.0)
9 | aws-partitions (1.300.0)
10 | aws-sdk-core (3.94.0)
11 | aws-eventstream (~> 1, >= 1.0.2)
12 | aws-partitions (~> 1, >= 1.239.0)
13 | aws-sigv4 (~> 1.1)
14 | jmespath (~> 1.0)
15 | aws-sdk-kms (1.30.0)
16 | aws-sdk-core (~> 3, >= 3.71.0)
17 | aws-sigv4 (~> 1.1)
18 | aws-sdk-s3 (1.62.0)
19 | aws-sdk-core (~> 3, >= 3.83.0)
20 | aws-sdk-kms (~> 1)
21 | aws-sigv4 (~> 1.1)
22 | aws-sigv4 (1.1.2)
23 | aws-eventstream (~> 1.0, >= 1.0.2)
24 | babosa (1.0.3)
25 | claide (1.0.3)
26 | colored (1.2)
27 | colored2 (3.1.2)
28 | commander-fastlane (4.4.6)
29 | highline (~> 1.7.2)
30 | declarative (0.0.10)
31 | declarative-option (0.1.0)
32 | digest-crc (0.5.1)
33 | domain_name (0.5.20190701)
34 | unf (>= 0.0.5, < 1.0.0)
35 | dotenv (2.7.5)
36 | emoji_regex (1.0.1)
37 | excon (0.73.0)
38 | faraday (0.17.3)
39 | multipart-post (>= 1.2, < 3)
40 | faraday-cookie_jar (0.0.6)
41 | faraday (>= 0.7.4)
42 | http-cookie (~> 1.0.0)
43 | faraday_middleware (0.13.1)
44 | faraday (>= 0.7.4, < 1.0)
45 | fastimage (2.1.7)
46 | fastlane (2.146.0)
47 | CFPropertyList (>= 2.3, < 4.0.0)
48 | addressable (>= 2.3, < 3.0.0)
49 | aws-sdk-s3 (~> 1.0)
50 | babosa (>= 1.0.2, < 2.0.0)
51 | bundler (>= 1.12.0, < 3.0.0)
52 | colored
53 | commander-fastlane (>= 4.4.6, < 5.0.0)
54 | dotenv (>= 2.1.1, < 3.0.0)
55 | emoji_regex (>= 0.1, < 2.0)
56 | excon (>= 0.71.0, < 1.0.0)
57 | faraday (~> 0.17)
58 | faraday-cookie_jar (~> 0.0.6)
59 | faraday_middleware (~> 0.13.1)
60 | fastimage (>= 2.1.0, < 3.0.0)
61 | gh_inspector (>= 1.1.2, < 2.0.0)
62 | google-api-client (>= 0.29.2, < 0.37.0)
63 | google-cloud-storage (>= 1.15.0, < 2.0.0)
64 | highline (>= 1.7.2, < 2.0.0)
65 | json (< 3.0.0)
66 | jwt (~> 2.1.0)
67 | mini_magick (>= 4.9.4, < 5.0.0)
68 | multi_xml (~> 0.5)
69 | multipart-post (~> 2.0.0)
70 | plist (>= 3.1.0, < 4.0.0)
71 | public_suffix (~> 2.0.0)
72 | rubyzip (>= 1.3.0, < 2.0.0)
73 | security (= 0.1.3)
74 | simctl (~> 1.6.3)
75 | slack-notifier (>= 2.0.0, < 3.0.0)
76 | terminal-notifier (>= 2.0.0, < 3.0.0)
77 | terminal-table (>= 1.4.5, < 2.0.0)
78 | tty-screen (>= 0.6.3, < 1.0.0)
79 | tty-spinner (>= 0.8.0, < 1.0.0)
80 | word_wrap (~> 1.0.0)
81 | xcodeproj (>= 1.13.0, < 2.0.0)
82 | xcpretty (~> 0.3.0)
83 | xcpretty-travis-formatter (>= 0.0.3)
84 | fastlane-plugin-increment_version_code (0.4.3)
85 | gh_inspector (1.1.3)
86 | google-api-client (0.36.4)
87 | addressable (~> 2.5, >= 2.5.1)
88 | googleauth (~> 0.9)
89 | httpclient (>= 2.8.1, < 3.0)
90 | mini_mime (~> 1.0)
91 | representable (~> 3.0)
92 | retriable (>= 2.0, < 4.0)
93 | signet (~> 0.12)
94 | google-cloud-core (1.5.0)
95 | google-cloud-env (~> 1.0)
96 | google-cloud-errors (~> 1.0)
97 | google-cloud-env (1.3.1)
98 | faraday (>= 0.17.3, < 2.0)
99 | google-cloud-errors (1.0.0)
100 | google-cloud-storage (1.26.0)
101 | addressable (~> 2.5)
102 | digest-crc (~> 0.4)
103 | google-api-client (~> 0.33)
104 | google-cloud-core (~> 1.2)
105 | googleauth (~> 0.9)
106 | mini_mime (~> 1.0)
107 | googleauth (0.12.0)
108 | faraday (>= 0.17.3, < 2.0)
109 | jwt (>= 1.4, < 3.0)
110 | memoist (~> 0.16)
111 | multi_json (~> 1.11)
112 | os (>= 0.9, < 2.0)
113 | signet (~> 0.14)
114 | highline (1.7.10)
115 | http-cookie (1.0.3)
116 | domain_name (~> 0.5)
117 | httpclient (2.8.3)
118 | jmespath (1.4.0)
119 | json (2.3.0)
120 | jwt (2.1.0)
121 | memoist (0.16.2)
122 | mini_magick (4.10.1)
123 | mini_mime (1.0.2)
124 | multi_json (1.14.1)
125 | multi_xml (0.6.0)
126 | multipart-post (2.0.0)
127 | nanaimo (0.2.6)
128 | naturally (2.2.0)
129 | os (1.1.0)
130 | plist (3.5.0)
131 | public_suffix (2.0.5)
132 | representable (3.0.4)
133 | declarative (< 0.1.0)
134 | declarative-option (< 0.2.0)
135 | uber (< 0.2.0)
136 | retriable (3.1.2)
137 | rouge (2.0.7)
138 | rubyzip (1.3.0)
139 | security (0.1.3)
140 | signet (0.14.0)
141 | addressable (~> 2.3)
142 | faraday (>= 0.17.3, < 2.0)
143 | jwt (>= 1.5, < 3.0)
144 | multi_json (~> 1.10)
145 | simctl (1.6.8)
146 | CFPropertyList
147 | naturally
148 | slack-notifier (2.3.2)
149 | terminal-notifier (2.0.0)
150 | terminal-table (1.8.0)
151 | unicode-display_width (~> 1.1, >= 1.1.1)
152 | tty-cursor (0.7.1)
153 | tty-screen (0.7.1)
154 | tty-spinner (0.9.3)
155 | tty-cursor (~> 0.7)
156 | uber (0.1.0)
157 | unf (0.1.4)
158 | unf_ext
159 | unf_ext (0.0.7.7)
160 | unicode-display_width (1.7.0)
161 | word_wrap (1.0.0)
162 | xcodeproj (1.16.0)
163 | CFPropertyList (>= 2.3.3, < 4.0)
164 | atomos (~> 0.1.3)
165 | claide (>= 1.0.2, < 2.0)
166 | colored2 (~> 3.1)
167 | nanaimo (~> 0.2.6)
168 | xcpretty (0.3.0)
169 | rouge (~> 2.0.7)
170 | xcpretty-travis-formatter (1.0.0)
171 | xcpretty (~> 0.2, >= 0.0.7)
172 |
173 | PLATFORMS
174 | ruby
175 |
176 | DEPENDENCIES
177 | fastlane
178 | fastlane-plugin-increment_version_code
179 |
180 | BUNDLED WITH
181 | 2.1.2
182 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_sign_up.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
23 |
24 |
30 |
31 |
32 |
33 |
42 |
43 |
49 |
50 |
51 |
52 |
61 |
62 |
68 |
69 |
70 |
71 |
80 |
81 |
87 |
88 |
89 |
90 |
101 |
102 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 | # Determine the Java command to use to start the JVM.
86 | if [ -n "$JAVA_HOME" ] ; then
87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
88 | # IBM's JDK on AIX uses strange locations for the executables
89 | JAVACMD="$JAVA_HOME/jre/sh/java"
90 | else
91 | JAVACMD="$JAVA_HOME/bin/java"
92 | fi
93 | if [ ! -x "$JAVACMD" ] ; then
94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
95 |
96 | Please set the JAVA_HOME variable in your environment to match the
97 | location of your Java installation."
98 | fi
99 | else
100 | JAVACMD="java"
101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
102 |
103 | Please set the JAVA_HOME variable in your environment to match the
104 | location of your Java installation."
105 | fi
106 |
107 | # Increase the maximum file descriptors if we can.
108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
109 | MAX_FD_LIMIT=`ulimit -H -n`
110 | if [ $? -eq 0 ] ; then
111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
112 | MAX_FD="$MAX_FD_LIMIT"
113 | fi
114 | ulimit -n $MAX_FD
115 | if [ $? -ne 0 ] ; then
116 | warn "Could not set maximum file descriptor limit: $MAX_FD"
117 | fi
118 | else
119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
120 | fi
121 | fi
122 |
123 | # For Darwin, add options to specify how the application appears in the dock
124 | if $darwin; then
125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
126 | fi
127 |
128 | # For Cygwin or MSYS, switch paths to Windows format before running java
129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
132 | JAVACMD=`cygpath --unix "$JAVACMD"`
133 |
134 | # We build the pattern for arguments to be converted via cygpath
135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
136 | SEP=""
137 | for dir in $ROOTDIRSRAW ; do
138 | ROOTDIRS="$ROOTDIRS$SEP$dir"
139 | SEP="|"
140 | done
141 | OURCYGPATTERN="(^($ROOTDIRS))"
142 | # Add a user-defined pattern to the cygpath arguments
143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
145 | fi
146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
147 | i=0
148 | for arg in "$@" ; do
149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
151 |
152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
154 | else
155 | eval `echo args$i`="\"$arg\""
156 | fi
157 | i=`expr $i + 1`
158 | done
159 | case $i in
160 | 0) set -- ;;
161 | 1) set -- "$args0" ;;
162 | 2) set -- "$args0" "$args1" ;;
163 | 3) set -- "$args0" "$args1" "$args2" ;;
164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
170 | esac
171 | fi
172 |
173 | # Escape application args
174 | save () {
175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
176 | echo " "
177 | }
178 | APP_ARGS=`save "$@"`
179 |
180 | # Collect all arguments for the java command, following the shell quoting and substitution rules
181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
182 |
183 | exec "$JAVACMD" "$@"
184 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://codeclimate.com/repos/5cd1d8c8af2ce517db016a12/maintainability)
2 | [](https://opensource.org/licenses/MIT)
3 |
4 | # Android Base
5 |
6 | **Android Base** is a boilerplate project created by Rootstrap for new projects using Kotlin 1.3.61. The main objective is helping any new projects jump start into feature development by providing a handful of functionalities.
7 |
8 | ## Prerequisites
9 | - Android SDK
10 | - Android Studio
11 | - Firebase google-services.json file.
12 | - Change your release key information on the build.gradle:
13 | ```
14 | signingConfigs {
15 | releaseConfig {
16 | keyAlias setAlias
17 | keyPassword setPassword
18 | storeFile file(setStoreFile)
19 | storePassword setStorePassword
20 | }
21 | }
22 | ```
23 | - Build the project with Android Studio.
24 |
25 | ## Installation
26 | 1. Clone
27 |
28 | 2. Build with Android Studio
29 |
30 | To manage user and session after sign in/up we store that information in Preferences. The parameters that we save are due to the usage of Device Token Auth for authentication on the server side.
31 |
32 | Please Check
33 | ```
34 | ResponseInterceptor.kt
35 | AuthenticationInterceptor.kt
36 | ```
37 | to handle the server side authentication, in case you need to modify them:
38 |
39 | ## Usage
40 | - You can use this open source project as a template of your new Android projects.
41 |
42 | ## Key File encryption
43 |
44 | Build signing requires a developer-owned keystore. Location and credentials for it are specified in `gradle.properties`. Likewise submission to Google Play requires a Developer API key in .json format (`google-api.json`).
45 | It is recommended that these files remains outside the source repo
46 |
47 | We suggest using [git secret](https://git-secret.io/) as a simple and secure solution for keeping these sensitive files in the repo. See [Config](./secure/Readme.md) for detailed instructions.
48 |
49 |
50 | ## Build and Release with Fastlane
51 |
52 | We provide configuration files for automating build, test and submission of the application using [Fastlane](https://docs.fastlane.tools/)
53 |
54 | ### Requirements
55 |
56 | * Ensure JDK 1.8 is installed
57 | * Ensure proper version of Android SDK command line tools is installed
58 | * Install _fastlane_ using
59 | ```
60 | [sudo] gem install fastlane -NV
61 | ```
62 | or alternatively using `brew cask install fastlane`
63 |
64 | ### Usage
65 | Lanes for each deployment target example are provided with some basic behavior:
66 | - Each target has two options: `debug_x` and `deploy_x`.
67 | - Each option will:
68 | - Increment the build number.
69 | - Run `gradlew clean`
70 | - Run `gradlew androidDependencies`
71 | - Build the app (`gradle assemble`) for the target flavor.
72 | - The `deploy` lanes will additionaly submit the APK to the corresponding track in the Play Store.
73 |
74 | Check `fastlane/Appfile` and `fastlane/Fastfile` for more information.
75 |
76 | ## CI/CD configuration with Bitrise (updated on Dec 12th 2021)
77 |
78 | We are going to start using a tool called Bitrise to configure the CI/CD pipelines for mobiles apps.
79 |
80 | --> For Android apps you can find how to do it in this link: https://www.notion.so/rootstrap/Android-CI-CD-26d4abd4f2454224be8f617110147366
81 |
82 | ## Continuous Integration with GitHub Actions (DEPRECATED)
83 |
84 | We provide an example workflow [cicd.yml](.github/workflows/cicd.yml) including two jobs for running under [GitHub Actions](https://docs.github.com/en/actions), which can be modified according to the specifics of each project:
85 |
86 | * `ci`
87 | * runs upon every push and PR
88 | * installs Fastlane and runs `debug_dev` lane
89 | * `release`
90 | * runs upon every push to `develop` or `master`
91 | * downloads keystore and Google api key from S3 (credentials need to be present in repo Secrets)
92 | * installs Fastlane and runs `deploy_*` lane depending on branch (`Dev` if in `develop`, `Stsaging` if in `master`) - This could be easily modified to release `Prod` instead
93 |
94 | ## Analytics
95 | - Add analytics manager:
96 | 1. Firebase
97 | 2. MixPanel[Optional]
98 |
99 | **How use:**
100 | In the Application class -> onCreate
101 | ```
102 | Analytics.addProvider(GoogleAnalytics(applicationContext))
103 | Analytics.addProvider(MixPanelAnalytics(applicationContext))
104 | ```
105 | or an array of providers
106 | `Analytics.addProviders(arrayOfProviders)`
107 |
108 | then use:
109 | `Analytics.track(PageEvents.visit(VISIT_MAIN))`
110 | or for events
111 | `Analytics.track(UserEvents.login())`
112 | in order to track the login event.
113 |
114 | - For firebase replace the file: google-services.json with the once for your App and follow the Firebase instructions.
115 | - For MixPanel, you have to replace the API key:
116 | `mixpanel_api_key`
117 |
118 |
119 | ## Utility extensions
120 | We have a bunch of pre-made extensions usually used on every project to accelerate feature development.
121 | you can access them in the util.extensions package. They include but are not limited to :
122 |
123 | - `Fragment.collectOnLifeCycle(...)` extension to reduce the boiler plate code and indentation when
124 | collecting flows from a fragment.
125 | - `ProgressBar.progressTo(...)` extension to animate progress updates in one line with ease
126 | and with good support for older android versions.
127 | - `TextView.setClickableKeyword(...)` extension to add clickable functions to words or prhases inside a
128 | Textview, allowing to also change their color, font (for example, to bold the keyword), and underline
129 | - `TextView.setColoredKeyword(...) ` extension to change the color of a word or prhase in a Textview.
130 |
131 |
132 | ## Code Quality Standards
133 | In order to meet the required code quality standards, this project uses [Ktlint](https://github.com/pinterest/ktlint) and [Detekt](https://github.com/arturbosch/detekt)
134 |
135 | ## Contributing
136 | Bug reports (please use Issues) and pull requests are welcome on GitHub at [android-base](https://github.com/rootstrap/android-base). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
137 |
138 | ## License
139 | The library is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
140 |
141 | NOTE: Remove the free LICENSE file for private projects or replace it with the corresponding license.
142 |
143 | ## Credits
144 | **Android Base** is maintained by [Rootstrap](http://www.rootstrap.com) with the help of our [contributors](https://github.com/rootstrap/android-base/contributors).
145 |
146 | [
](http://www.rootstrap.com)
147 |
148 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-kapt'
6 |
7 | apply plugin: 'dagger.hilt.android.plugin'
8 |
9 | apply plugin: 'com.google.gms.google-services'
10 |
11 | apply plugin: 'com.google.firebase.crashlytics'
12 |
13 | android {
14 | compileSdkVersion 31
15 | defaultConfig {
16 | applicationId "com.rootstrap.android"
17 | minSdkVersion 23
18 | targetSdkVersion 31
19 | versionCode 42
20 | versionName "1.0"
21 | testInstrumentationRunner 'com.rootstrap.android.CustomTestRunner'
22 | }
23 |
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 |
29 | buildFeatures {
30 | viewBinding true
31 | dataBinding true
32 | }
33 |
34 | kotlinOptions {
35 | jvmTarget = "1.8"
36 | }
37 |
38 | signingConfigs {
39 | releaseConfig {
40 | keyAlias projectKeyAlias
41 | keyPassword projectKeyPassword
42 | storeFile file(projectStoreFile)
43 | storePassword projectStorePassword
44 | }
45 | }
46 |
47 | buildTypes {
48 | debug {
49 | minifyEnabled false
50 | }
51 |
52 | release {
53 | signingConfig signingConfigs.releaseConfig
54 | minifyEnabled false
55 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
56 | }
57 | }
58 |
59 | kapt {
60 | correctErrorTypes true
61 | }
62 |
63 | flavorDimensions "server"
64 |
65 | productFlavors {
66 | dev {
67 | dimension "server"
68 | applicationIdSuffix ".dev"
69 | versionNameSuffix "-dev"
70 | buildConfigField("String", "API_URL", "\"https://rails5-api-base.herokuapp.com/api/v1/\"")
71 | buildConfigField("String", "SECURE_KEY_ALIAS", "\"$projectKeyAlias\"")
72 | buildConfigField("String", "SECURE_FILE_NAME", "\"appPreferencesDev\"")
73 | }
74 |
75 | staging {
76 | dimension "server"
77 | applicationIdSuffix ".staging"
78 | versionNameSuffix "-staging"
79 | buildConfigField("String", "API_URL", "\"https://proj-staging.herokuapp.com/api/\"")
80 | buildConfigField("String", "SECURE_KEY_ALIAS", "\"$projectKeyAlias\"")
81 | buildConfigField("String", "SECURE_FILE_NAME", "\"appPreferencesStaging\"")
82 | }
83 |
84 | prod {
85 | dimension "server"
86 | buildConfigField("String", "API_URL", "\"https://proj-production.herokuapp.com/api/\"")
87 | buildConfigField("String", "SECURE_KEY_ALIAS", "\"$projectKeyAlias\"")
88 | buildConfigField("String", "SECURE_FILE_NAME", "\"appPreferences\"")
89 | }
90 | }
91 |
92 | applicationVariants.all { variant ->
93 | variant.outputs.all { output ->
94 | def apk = output.outputFile
95 | def newName = apk.name.replace(".apk", "-v" + variant.versionName + ".apk")
96 | newName = newName.replace("-" + variant.buildType.name, "")
97 |
98 | outputFileName = new File("./apks/" + newName)
99 | }
100 | }
101 |
102 | configurations {
103 | ktlint
104 | }
105 |
106 | kotlinOptions {
107 | freeCompilerArgs = ["-Xallow-result-return-type"]
108 | }
109 |
110 | task ktlint(type: JavaExec, group: "verification") {
111 | description = "Check Kotlin code style."
112 | classpath = configurations.ktlint
113 | main = "com.pinterest.ktlint.Main"
114 | args "src/**/*.kt"
115 | // to generate report in checkstyle format prepend following args:
116 | // "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/ktlint.xml"
117 | // see https://github.com/pinterest/ktlint#usage for more
118 | }
119 |
120 | check.dependsOn ktlint
121 |
122 | task ktlintFormat(type: JavaExec, group: "formatting") {
123 | description = "Fix Kotlin code style deviations."
124 | classpath = configurations.ktlint
125 | main = "com.pinterest.ktlint.Main"
126 | args "-F", "src/**/*.kt"
127 | }
128 |
129 | useLibrary 'android.test.runner'
130 | useLibrary 'android.test.base'
131 | useLibrary 'android.test.mock'
132 |
133 | testOptions {
134 | unitTests {
135 | includeAndroidResources = true
136 | animationsDisabled = true
137 | }
138 | }
139 | }
140 |
141 | dependencies {
142 | def room_version = "2.3.0"
143 | def lifecycle_version = "2.4.0"
144 | def mockkVersion = '1.12.0'
145 |
146 | implementation fileTree(include: ['*.jar'], dir: 'libs')
147 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1'
148 | implementation 'androidx.appcompat:appcompat:1.3.1'
149 | implementation 'androidx.core:core-ktx:1.7.0'
150 | implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
151 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
152 | implementation "androidx.preference:preference-ktx:1.1.1"
153 | implementation 'com.google.android.material:material:1.4.0'
154 | testImplementation 'junit:junit:4.13.1'
155 | testImplementation 'org.mockito:mockito-core:2.28.2'
156 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1"
157 | testImplementation 'android.arch.core:core-testing:1.1.1'
158 | testImplementation "io.mockk:mockk:$mockkVersion"
159 |
160 | androidTestImplementation 'androidx.test:runner:1.4.0'
161 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
162 | androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
163 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
164 | androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
165 | androidTestImplementation 'androidx.test:rules:1.4.0'
166 | androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.3.1'
167 |
168 | //---- ROOM ----
169 | implementation "androidx.room:room-runtime:$room_version"
170 | kapt "androidx.room:room-compiler:$room_version"
171 | // Kotlin Extensions and Coroutines support for Room
172 | implementation "androidx.room:room-ktx:$room_version"
173 |
174 | //---- LIFECYCLE ----]
175 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
176 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
177 | implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
178 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
179 | implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
180 |
181 | //---- GOOGLE JSON SERIALIZER/DESERIALIZER ----
182 | implementation 'com.google.code.gson:gson:2.8.6'
183 |
184 | //---- MixPanel ----
185 | implementation 'com.mixpanel.android:mixpanel-android:5.6.1'
186 |
187 | //---- Firebase ----
188 | implementation platform('com.google.firebase:firebase-bom:28.4.2')
189 | implementation 'com.google.firebase:firebase-core:20.0.0'
190 | implementation 'com.google.firebase:firebase-analytics-ktx'
191 | implementation 'com.google.firebase:firebase-crashlytics-ktx'
192 | implementation 'org.jetbrains.kotlin:kotlin-reflect:1.5.31'
193 |
194 | //---- Image ----
195 | implementation group: 'com.github.bumptech.glide', name: 'glide', version: '4.10.0'
196 |
197 | //---- Network ----
198 | implementation 'com.squareup.retrofit2:retrofit:2.6.2'
199 | implementation 'com.squareup.moshi:moshi-kotlin:1.12.0'
200 | implementation 'com.squareup.retrofit2:converter-moshi:2.5.0'
201 | implementation 'com.squareup.okhttp3:logging-interceptor:4.3.1'
202 |
203 | //---- Events ----
204 | implementation 'com.squareup:otto:1.3.8'
205 |
206 | //---- Linters ----
207 | ktlint "com.pinterest:ktlint:0.35.0"
208 |
209 | //---- Hilt ----
210 | implementation "com.google.dagger:hilt-android:$hilt_version"
211 | kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
212 | kapt 'androidx.hilt:hilt-compiler:1.0.0'
213 | androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
214 | kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
215 |
216 | //security crypto
217 | implementation "androidx.security:security-crypto:1.1.0-alpha03"
218 |
219 | //nav component
220 | implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
221 | implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
222 | }
223 |
--------------------------------------------------------------------------------
/default-detekt-config.yml:
--------------------------------------------------------------------------------
1 | autoCorrect: true
2 | failFast: false
3 |
4 | test-pattern: # Configure exclusions for test sources
5 | active: true
6 | patterns: # Test file regexes
7 | - '.*/test/.*'
8 | - '.*Test.kt'
9 | - '.*Spec.kt'
10 | exclude-rule-sets:
11 | - 'comments'
12 | exclude-rules:
13 | - 'NamingRules'
14 | - 'MagicNumber'
15 | - 'MaxLineLength'
16 | - 'LateinitUsage'
17 | - 'StringLiteralDuplication'
18 | - 'SpreadOperator'
19 | - 'TooManyFunctions'
20 |
21 | build:
22 | warningThreshold: 5
23 | failThreshold: 10
24 | weights:
25 | complexity: 2
26 | formatting: 1
27 | LongParameterList: 1
28 | comments: 1
29 |
30 | processors:
31 | active: true
32 | exclude:
33 | # - 'FunctionCountProcessor'
34 | # - 'PropertyCountProcessor'
35 | # - 'ClassCountProcessor'
36 | # - 'PackageCountProcessor'
37 | # - 'KtFileCountProcessor'
38 |
39 | console-reports:
40 | active: true
41 | exclude:
42 | # - 'ProjectStatisticsReport'
43 | # - 'ComplexityReport'
44 | # - 'NotificationReport'
45 | # - 'FindingsReport'
46 | # - 'BuildFailureReport'
47 |
48 | output-reports:
49 | active: true
50 | exclude:
51 | # - 'PlainOutputReport'
52 | # - 'XmlOutputReport'
53 |
54 | comments:
55 | active: true
56 | CommentOverPrivateFunction:
57 | active: false
58 | CommentOverPrivateProperty:
59 | active: false
60 | EndOfSentenceFormat:
61 | active: false
62 | endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$)
63 | UndocumentedPublicClass:
64 | active: false
65 | searchInNestedClass: true
66 | searchInInnerClass: true
67 | searchInInnerObject: true
68 | searchInInnerInterface: true
69 | UndocumentedPublicFunction:
70 | active: false
71 |
72 | complexity:
73 | active: true
74 | ComplexCondition:
75 | active: true
76 | threshold: 3
77 | ComplexInterface:
78 | active: false
79 | threshold: 10
80 | includeStaticDeclarations: false
81 | ComplexMethod:
82 | active: true
83 | threshold: 10
84 | LabeledExpression:
85 | active: false
86 | LargeClass:
87 | active: true
88 | threshold: 150
89 | LongMethod:
90 | active: true
91 | threshold: 20
92 | LongParameterList:
93 | active: true
94 | threshold: 5
95 | ignoreDefaultParameters: false
96 | MethodOverloading:
97 | active: false
98 | threshold: 5
99 | NestedBlockDepth:
100 | active: true
101 | threshold: 3
102 | StringLiteralDuplication:
103 | active: false
104 | threshold: 2
105 | ignoreAnnotation: true
106 | excludeStringsWithLessThan5Characters: true
107 | ignoreStringsRegex: '$^'
108 | TooManyFunctions:
109 | active: true
110 | thresholdInFiles: 10
111 | thresholdInClasses: 10
112 | thresholdInInterfaces: 10
113 | thresholdInObjects: 10
114 | thresholdInEnums: 10
115 |
116 | empty-blocks:
117 | active: true
118 | EmptyCatchBlock:
119 | active: true
120 | EmptyClassBlock:
121 | active: true
122 | EmptyDefaultConstructor:
123 | active: true
124 | EmptyDoWhileBlock:
125 | active: true
126 | EmptyElseBlock:
127 | active: true
128 | EmptyFinallyBlock:
129 | active: true
130 | EmptyForBlock:
131 | active: true
132 | EmptyFunctionBlock:
133 | active: true
134 | EmptyIfBlock:
135 | active: true
136 | EmptyInitBlock:
137 | active: true
138 | EmptyKtFile:
139 | active: true
140 | EmptySecondaryConstructor:
141 | active: true
142 | EmptyWhenBlock:
143 | active: true
144 | EmptyWhileBlock:
145 | active: true
146 |
147 | exceptions:
148 | active: true
149 | ExceptionRaisedInUnexpectedLocation:
150 | active: false
151 | methodNames: 'toString,hashCode,equals,finalize'
152 | InstanceOfCheckForException:
153 | active: false
154 | NotImplementedDeclaration:
155 | active: false
156 | PrintStackTrace:
157 | active: false
158 | RethrowCaughtException:
159 | active: false
160 | ReturnFromFinally:
161 | active: false
162 | SwallowedException:
163 | active: false
164 | ThrowingExceptionFromFinally:
165 | active: false
166 | ThrowingExceptionInMain:
167 | active: false
168 | ThrowingExceptionsWithoutMessageOrCause:
169 | active: false
170 | exceptions: 'IllegalArgumentException,IllegalStateException,IOException'
171 | ThrowingNewInstanceOfSameException:
172 | active: false
173 | TooGenericExceptionCaught:
174 | active: true
175 | exceptions:
176 | - ArrayIndexOutOfBoundsException
177 | - Error
178 | - Exception
179 | - IllegalMonitorStateException
180 | - NullPointerException
181 | - IndexOutOfBoundsException
182 | - RuntimeException
183 | - Throwable
184 | TooGenericExceptionThrown:
185 | active: true
186 | exceptions:
187 | - Error
188 | - Exception
189 | - NullPointerException
190 | - Throwable
191 | - RuntimeException
192 |
193 | naming:
194 | active: true
195 | ClassNaming:
196 | active: true
197 | classPattern: '[A-Z$][a-zA-Z0-9$]*'
198 | EnumNaming:
199 | active: true
200 | enumEntryPattern: '^[A-Z$][a-zA-Z_$]*$'
201 | ForbiddenClassName:
202 | active: false
203 | forbiddenName: ''
204 | FunctionMaxLength:
205 | active: false
206 | maximumFunctionNameLength: 30
207 | FunctionMinLength:
208 | active: false
209 | minimumFunctionNameLength: 3
210 | FunctionNaming:
211 | active: true
212 | functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
213 | MatchingDeclarationName:
214 | active: true
215 | MemberNameEqualsClassName:
216 | active: false
217 | ignoreOverriddenFunction: true
218 | ObjectPropertyNaming:
219 | active: true
220 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
221 | PackageNaming:
222 | active: true
223 | packagePattern: '^[a-z]+(\.[a-z][a-z0-9]*)*$'
224 | TopLevelPropertyNaming:
225 | active: true
226 | constantPattern: '[A-Z][_A-Z0-9]*'
227 | propertyPattern: '[a-z][A-Za-z\d]*'
228 | privatePropertyPattern: '(_)?[a-z][A-Za-z0-9]*'
229 | VariableMaxLength:
230 | active: false
231 | maximumVariableNameLength: 64
232 | VariableMinLength:
233 | active: false
234 | minimumVariableNameLength: 1
235 | VariableNaming:
236 | active: true
237 | variablePattern: '[a-z][A-Za-z0-9]*'
238 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
239 |
240 | performance:
241 | active: true
242 | ForEachOnRange:
243 | active: true
244 | SpreadOperator:
245 | active: true
246 | UnnecessaryTemporaryInstantiation:
247 | active: true
248 |
249 | potential-bugs:
250 | active: true
251 | DuplicateCaseInWhenExpression:
252 | active: true
253 | EqualsAlwaysReturnsTrueOrFalse:
254 | active: false
255 | EqualsWithHashCodeExist:
256 | active: true
257 | ExplicitGarbageCollectionCall:
258 | active: true
259 | InvalidRange:
260 | active: false
261 | IteratorHasNextCallsNextMethod:
262 | active: false
263 | IteratorNotThrowingNoSuchElementException:
264 | active: false
265 | LateinitUsage:
266 | active: false
267 | excludeAnnotatedProperties: ""
268 | ignoreOnClassesPattern: ""
269 | UnconditionalJumpStatementInLoop:
270 | active: false
271 | UnreachableCode:
272 | active: true
273 | UnsafeCallOnNullableType:
274 | active: false
275 | UnsafeCast:
276 | active: false
277 | UselessPostfixExpression:
278 | active: false
279 | WrongEqualsTypeParameter:
280 | active: false
281 |
282 | style:
283 | active: true
284 | CollapsibleIfStatements:
285 | active: false
286 | DataClassContainsFunctions:
287 | active: false
288 | conversionFunctionPrefix: 'to'
289 | EqualsNullCall:
290 | active: false
291 | ExpressionBodySyntax:
292 | active: false
293 | ForbiddenComment:
294 | active: true
295 | values: 'TODO:,FIXME:,STOPSHIP:'
296 | ForbiddenImport:
297 | active: false
298 | imports: ''
299 | FunctionOnlyReturningConstant:
300 | active: false
301 | ignoreOverridableFunction: true
302 | excludedFunctions: 'describeContents'
303 | LoopWithTooManyJumpStatements:
304 | active: false
305 | maxJumpCount: 1
306 | MagicNumber:
307 | active: true
308 | ignoreNumbers: '-1,0,1,2'
309 | ignoreHashCodeFunction: false
310 | ignorePropertyDeclaration: false
311 | ignoreConstantDeclaration: true
312 | ignoreCompanionObjectPropertyDeclaration: true
313 | ignoreAnnotation: false
314 | ignoreNamedArgument: true
315 | ignoreEnums: false
316 | MaxLineLength:
317 | active: true
318 | maxLineLength: 120
319 | excludePackageStatements: false
320 | excludeImportStatements: false
321 | ModifierOrder:
322 | active: true
323 | NestedClassesVisibility:
324 | active: false
325 | NewLineAtEndOfFile:
326 | active: true
327 | OptionalAbstractKeyword:
328 | active: true
329 | OptionalReturnKeyword:
330 | active: false
331 | OptionalUnit:
332 | active: false
333 | OptionalWhenBraces:
334 | active: false
335 | ProtectedMemberInFinalClass:
336 | active: false
337 | RedundantVisibilityModifierRule:
338 | active: false
339 | ReturnCount:
340 | active: true
341 | max: 2
342 | excludedFunctions: "equals"
343 | SafeCast:
344 | active: true
345 | SerialVersionUIDInSerializableClass:
346 | active: false
347 | SpacingBetweenPackageAndImports:
348 | active: false
349 | ThrowsCount:
350 | active: true
351 | max: 2
352 | UnnecessaryAbstractClass:
353 | active: false
354 | UnnecessaryInheritance:
355 | active: false
356 | UnnecessaryParentheses:
357 | active: false
358 | UntilInsteadOfRangeTo:
359 | active: false
360 | UnusedImports:
361 | active: false
362 | UseDataClass:
363 | active: false
364 | excludeAnnotatedClasses: ""
365 | UtilityClassWithPublicConstructor:
366 | active: false
367 | WildcardImport:
368 | active: true
369 | excludeImports: 'java.util.*,kotlinx.android.synthetic.*'
--------------------------------------------------------------------------------