├── 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 | 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 |