├── app ├── .gitignore ├── src │ ├── main │ │ ├── ic_launcher-web.png │ │ ├── ic_background-web.png │ │ ├── ic_placeholder-web.png │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── preloaded_fonts.xml │ │ │ │ └── colors.xml │ │ │ ├── drawable │ │ │ │ ├── bg_order_pending.xml │ │ │ │ ├── bg_order_cancelled.xml │ │ │ │ ├── bg_order_completed.xml │ │ │ │ ├── bg_order_confirmed.xml │ │ │ │ ├── bg_order_in_progress.xml │ │ │ │ ├── plain_brown.xml │ │ │ │ ├── custom_item_divider.xml │ │ │ │ ├── background_splash.xml │ │ │ │ ├── ic_minus.xml │ │ │ │ ├── ic_arrow_back.xml │ │ │ │ ├── ic_add.xml │ │ │ │ ├── ic_location_pin.xml │ │ │ │ ├── ic_error.xml │ │ │ │ ├── ic_delete.xml │ │ │ │ ├── ic_mail.xml │ │ │ │ ├── ic_orders.xml │ │ │ │ ├── ic_cart_light.xml │ │ │ │ ├── ic_profile_light.xml │ │ │ │ ├── ic_cart.xml │ │ │ │ ├── ic_address_book.xml │ │ │ │ ├── ic_website.xml │ │ │ │ ├── ic_linkedin.xml │ │ │ │ ├── ic_just_java_logo_black.xml │ │ │ │ ├── ic_just_java_logo.xml │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── color │ │ │ │ ├── color_states_button.xml │ │ │ │ ├── color_states_button_filled.xml │ │ │ │ └── color_states_button_outline_text.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ └── ic_launcher.xml │ │ │ ├── font │ │ │ │ ├── lato.xml │ │ │ │ └── bree_serif.xml │ │ │ ├── menu │ │ │ │ └── overflow_menu.xml │ │ │ └── layout │ │ │ │ ├── dialog_reset_password.xml │ │ │ │ ├── item_product_choice_option.xml │ │ │ │ ├── item_product_choice.xml │ │ │ │ ├── activity_product_details.xml │ │ │ │ ├── item_address.xml │ │ │ │ ├── item_main_activity_shimmer.xml │ │ │ │ ├── item_cart_item.xml │ │ │ │ ├── item_product.xml │ │ │ │ ├── item_order_item.xml │ │ │ │ └── activity_orders.xml │ │ └── java │ │ │ └── com │ │ │ └── marknkamau │ │ │ └── justjava │ │ │ ├── data │ │ │ ├── models │ │ │ │ ├── NotificationReason.kt │ │ │ │ ├── CartItem.kt │ │ │ │ ├── CartProductEntity.kt │ │ │ │ ├── CartOptionEntity.kt │ │ │ │ └── AppProduct.kt │ │ │ ├── db │ │ │ │ ├── DbRepository.kt │ │ │ │ ├── AppDatabase.kt │ │ │ │ ├── CartDao.kt │ │ │ │ └── DbRepositoryImpl.kt │ │ │ ├── network │ │ │ │ ├── AppFirebaseService.kt │ │ │ │ └── GoogleSignInServiceImpl.kt │ │ │ └── preferences │ │ │ │ └── PreferencesRepositoryImpl.kt │ │ │ ├── utils │ │ │ ├── ViewUtils.kt │ │ │ ├── KeyboardUtils.kt │ │ │ ├── CurrencyFormatter.kt │ │ │ ├── PhoneNumberUtils.kt │ │ │ ├── ItemDiffCallback.kt │ │ │ ├── ReleaseTree.kt │ │ │ ├── Extensions.kt │ │ │ ├── EditTextExtensions.kt │ │ │ ├── DateTime.kt │ │ │ └── PreferenceUtils.kt │ │ │ ├── ui │ │ │ ├── SplashActivity.kt │ │ │ ├── productDetails │ │ │ │ ├── ProductDetailsViewModel.kt │ │ │ │ └── OptionsAdapter.kt │ │ │ ├── orders │ │ │ │ ├── OrdersViewModel.kt │ │ │ │ └── OrdersActivity.kt │ │ │ ├── main │ │ │ │ └── MainViewModel.kt │ │ │ ├── payMpesa │ │ │ │ └── PayMpesaViewModel.kt │ │ │ ├── signup │ │ │ │ └── SignUpViewModel.kt │ │ │ ├── base │ │ │ │ └── BaseActivity.kt │ │ │ ├── payCard │ │ │ │ └── PayCardViewModel.kt │ │ │ ├── addressBook │ │ │ │ ├── AddressAdapter.kt │ │ │ │ └── AddressBookViewModel.kt │ │ │ ├── orderDetail │ │ │ │ └── OrderDetailViewModel.kt │ │ │ ├── ToolbarActivity.kt │ │ │ ├── about │ │ │ │ └── AboutActivity.kt │ │ │ ├── cart │ │ │ │ └── CartViewModel.kt │ │ │ ├── login │ │ │ │ └── SignInViewModel.kt │ │ │ └── profile │ │ │ │ └── ProfileViewModel.kt │ │ │ ├── di │ │ │ ├── PreferencesModule.kt │ │ │ ├── DatabaseModule.kt │ │ │ ├── RepositoriesModule.kt │ │ │ └── NetworkModule.kt │ │ │ └── JustJavaApp.kt │ ├── debug │ │ ├── ic_launcher-web.png │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ └── drawable │ │ │ └── ic_launcher_foreground.xml │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── marknkamau │ │ │ └── justjava │ │ │ ├── testUtils │ │ │ ├── TestApplicationRunner.kt │ │ │ ├── EspressoUtils.kt │ │ │ ├── SampleData.kt │ │ │ └── TestRepositoriesModule.kt │ │ │ └── SmokeTest.kt │ └── test │ │ └── java │ │ └── com │ │ └── marknkamau │ │ └── justjava │ │ ├── utils │ │ ├── PhoneNumberUtilsTest.kt │ │ ├── SampleData.kt │ │ └── DateTimeTest.kt │ │ └── ui │ │ ├── productDetails │ │ └── ProductDetailsViewModelTest.kt │ │ ├── main │ │ └── MainViewModelTest.kt │ │ ├── orders │ │ └── OrdersViewModelTest.kt │ │ ├── payCard │ │ └── PayCardViewModelTest.kt │ │ ├── payMpesa │ │ └── PayMpesaViewModelTest.kt │ │ ├── orderDetail │ │ └── OrderDetailViewModelTest.kt │ │ ├── signup │ │ └── SignUpViewModelTest.kt │ │ └── profile │ │ └── ProfileViewModelTest.kt └── proguard-rules.pro ├── core ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ └── strings.xml │ │ │ └── xml │ │ │ │ └── network_security_config.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── marknjunge │ │ │ │ └── core │ │ │ │ ├── data │ │ │ │ ├── network │ │ │ │ │ ├── service │ │ │ │ │ │ ├── GoogleSignInService.kt │ │ │ │ │ │ ├── FirebaseService.kt │ │ │ │ │ │ ├── ApiService.kt │ │ │ │ │ │ ├── CartService.kt │ │ │ │ │ │ ├── PaymentsService.kt │ │ │ │ │ │ ├── OrdersService.kt │ │ │ │ │ │ ├── UsersService.kt │ │ │ │ │ │ └── AuthService.kt │ │ │ │ │ ├── NetworkCall.kt │ │ │ │ │ ├── interceptors │ │ │ │ │ │ ├── SessionIdInterceptor.kt │ │ │ │ │ │ ├── NetworkConnectionInterceptor.kt │ │ │ │ │ │ └── ConvertNoContentInterceptor.kt │ │ │ │ │ └── NetworkProvider.kt │ │ │ │ ├── model │ │ │ │ │ ├── PaymentStatus.kt │ │ │ │ │ ├── ErrorModel.kt │ │ │ │ │ ├── ErrorType.kt │ │ │ │ │ ├── Resource.kt │ │ │ │ │ ├── ApiResponse.kt │ │ │ │ │ ├── OrderStatus.kt │ │ │ │ │ ├── SignInGoogleDto.kt │ │ │ │ │ ├── UpdateFcmTokenDto.kt │ │ │ │ │ ├── RequestPasswordResetDto.kt │ │ │ │ │ ├── VerifyOrderDto.kt │ │ │ │ │ ├── ChangePaymentMethodDto.kt │ │ │ │ │ ├── SignInDto.kt │ │ │ │ │ ├── SignInResponse.kt │ │ │ │ │ ├── ResetPasswordDto.kt │ │ │ │ │ ├── RequestMpesaDto.kt │ │ │ │ │ ├── PlaceOrderItemOptionsDto.kt │ │ │ │ │ ├── PaymentMethod.kt │ │ │ │ │ ├── CardDetails.kt │ │ │ │ │ ├── Session.kt │ │ │ │ │ ├── VerifyOrderItemOptionDto.kt │ │ │ │ │ ├── PlaceOrderItemDto.kt │ │ │ │ │ ├── SaveAddressDto.kt │ │ │ │ │ ├── SignUpDto.kt │ │ │ │ │ ├── UpdateUserDto.kt │ │ │ │ │ ├── VerifyOrderItemDto.kt │ │ │ │ │ ├── PlaceOrderDto.kt │ │ │ │ │ ├── ProductChoiceOption.kt │ │ │ │ │ ├── Address.kt │ │ │ │ │ ├── OrderItemOption.kt │ │ │ │ │ ├── VerifyOrderResponse.kt │ │ │ │ │ ├── OrderItem.kt │ │ │ │ │ ├── ProductChoice.kt │ │ │ │ │ ├── Product.kt │ │ │ │ │ ├── Order.kt │ │ │ │ │ ├── User.kt │ │ │ │ │ └── InitiateCardPaymentDto.kt │ │ │ │ ├── local │ │ │ │ │ └── PreferencesRepository.kt │ │ │ │ └── repository │ │ │ │ │ ├── ProductsRepository.kt │ │ │ │ │ ├── CartRepository.kt │ │ │ │ │ ├── OrdersRepository.kt │ │ │ │ │ └── PaymentRepository.kt │ │ │ │ └── utils │ │ │ │ ├── JsonConfig.kt │ │ │ │ ├── NoInternetException.kt │ │ │ │ ├── Utils.kt │ │ │ │ ├── serialization │ │ │ │ └── PaymentMethodSerializer.kt │ │ │ │ └── ExceptionUtils.kt │ │ └── AndroidManifest.xml │ └── test │ │ └── java │ │ └── com │ │ └── marknjunge │ │ └── core │ │ ├── utils │ │ └── UtilsTest.kt │ │ ├── SampleData.kt │ │ └── data │ │ └── repository │ │ ├── ApiProductsRepositoryTest.kt │ │ └── ApiCartRepositoryTest.kt ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── keys.properties.sample ├── images ├── branding.png └── just_java_logo.png ├── keystore ├── justjava-debug.jks └── justjava-debug.txt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── Changelog.MD └── gradlew.bat /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':core' 2 | -------------------------------------------------------------------------------- /keys.properties.sample: -------------------------------------------------------------------------------- 1 | GOOGLE_MAPS_API_KEY="your_key" 2 | SENTRY_DSN="your_dsn" -------------------------------------------------------------------------------- /images/branding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/images/branding.png -------------------------------------------------------------------------------- /images/just_java_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/images/just_java_logo.png -------------------------------------------------------------------------------- /core/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | core 3 | 4 | -------------------------------------------------------------------------------- /keystore/justjava-debug.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/keystore/justjava-debug.jks -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/debug/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/debug/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/ic_background-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/main/ic_background-web.png -------------------------------------------------------------------------------- /app/src/main/ic_placeholder-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/main/ic_placeholder-web.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/debug/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/debug/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/debug/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkNjunge/JustJava-Android/HEAD/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #5C5C5C 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3E2723 4 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/service/GoogleSignInService.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network.service 2 | 3 | interface GoogleSignInService { 4 | suspend fun signOut() 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/PaymentStatus.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | enum class PaymentStatus(val s: String) { 4 | PAID("PAID"), 5 | UNPAID("UNPAID"), 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/service/FirebaseService.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network.service 2 | 3 | interface FirebaseService { 4 | suspend fun getFcmToken(): String 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/utils/JsonConfig.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.utils 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | val appJsonConfig: Json 6 | get() = Json { ignoreUnknownKeys = true } 7 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/ErrorModel.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | enum class ErrorModel(val s: String) { 4 | PRODUCT("PRODUCT"), 5 | CHOICE("CHOICE"), 6 | OPTION("OPTION") 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/ErrorType.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | enum class ErrorType(val s: String) { 4 | MISSING("MISSING"), 5 | PRICE_CHANGE("PRICE_CHANGE"), 6 | UNAVAILABLE("UNAVAILABLE") 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/preloaded_fonts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @font/bree_serif 5 | @font/lato 6 | 7 | 8 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/Resource.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | sealed class Resource { 4 | data class Success(val data: T) : Resource() 5 | data class Failure(val response: ApiResponse) : Resource() 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10.0.2.2 5 | 6 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/utils/NoInternetException.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.utils 2 | 3 | import java.io.IOException 4 | 5 | // Has to be IO exception. See https://stackoverflow.com/a/47058587 6 | class NoInternetException : IOException("Check your internet connection") 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jul 18 15:52:54 EAT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_order_pending.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_order_cancelled.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_order_completed.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_order_confirmed.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_states_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_states_button_filled.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_order_in_progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/local/PreferencesRepository.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.local 2 | 3 | import com.marknjunge.core.data.model.User 4 | 5 | interface PreferencesRepository { 6 | val isSignedIn: Boolean 7 | var sessionId: String 8 | var user: User? 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_states_button_outline_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/plain_brown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/ApiResponse.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApiResponse( 8 | @SerialName("message") 9 | val message: String 10 | ) 11 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/OrderStatus.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | enum class OrderStatus(val s: String) { 4 | PENDING("PENDING"), 5 | CONFIRMED("CONFIRMED"), 6 | IN_PROGRESS("IN_PROGRESS"), 7 | COMPLETED("COMPLETED"), 8 | CANCELLED("CANCELLED"), 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/service/ApiService.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network.service 2 | 3 | import com.marknjunge.core.data.model.Product 4 | import retrofit2.http.GET 5 | 6 | interface ApiService { 7 | @GET("products") 8 | suspend fun getProducts(): List 9 | } 10 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/data/models/NotificationReason.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.data.models 2 | 3 | enum class NotificationReason(val s: String) { 4 | PAYMENT_COMPLETED("PAYMENT_COMPLETED"), 5 | PAYMENT_CANCELLED("PAYMENT_CANCELLED"), 6 | ORDER_STATUS_UPDATED("ORDER_STATUS_UPDATED"), 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/custom_item_divider.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/SignInGoogleDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class SignInGoogleDto( 8 | @SerialName("idToken") 9 | val idToken: String 10 | ) 11 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/UpdateFcmTokenDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class UpdateFcmTokenDto( 8 | @SerialName("fcmToken") 9 | val fcmToken: String 10 | ) 11 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/RequestPasswordResetDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class RequestPasswordResetDto( 8 | @SerialName("email") 9 | val email: String 10 | ) 11 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/VerifyOrderDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class VerifyOrderDto( 8 | @SerialName("items") 9 | val items: List 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/ChangePaymentMethodDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ChangePaymentMethodDto( 8 | @SerialName("paymentMethod") 9 | val paymentMethod: PaymentMethod 10 | ) 11 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/SignInDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class SignInDto( 8 | @SerialName("email") 9 | val email: String, 10 | 11 | @SerialName("password") 12 | val password: String 13 | ) 14 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/SignInResponse.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class SignInResponse( 8 | @SerialName("user") 9 | val user: User, 10 | 11 | @SerialName("session") 12 | val session: Session 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/data/models/CartItem.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.data.models 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Relation 5 | 6 | data class CartItem( 7 | @Embedded val cartItem: CartProductEntity, 8 | @Relation(parentColumn = "id", entityColumn = "cart_products_row_id") 9 | var options: List 10 | ) 11 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/ResetPasswordDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ResetPasswordDto( 8 | @SerialName("token") 9 | val token: String, 10 | @SerialName("newPassword") 11 | val newPassword: String 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/res/font/lato.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/RequestMpesaDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class RequestMpesaDto( 8 | @SerialName("mobileNumber") 9 | val mobileNumber: String, 10 | 11 | @SerialName("orderId") 12 | val orderId: String 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/res/font/bree_serif.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/PlaceOrderItemOptionsDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class PlaceOrderItemOptionsDto( 8 | @SerialName("choiceId") 9 | val choiceId: Long, 10 | 11 | @SerialName("optionId") 12 | val optionId: Long 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_minus.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/PaymentMethod.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import com.marknjunge.core.utils.serialization.PaymentMethodSerializer 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable(with = PaymentMethodSerializer::class) 7 | enum class PaymentMethod(val s: String) { 8 | CASH("CASH"), 9 | MPESA("MPESA"), 10 | CARD("CARD") 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/CardDetails.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | data class CardDetails( 4 | val cardNo: String, 5 | val cvv: String, 6 | val expiryMonth: String, 7 | val expiryYear: String, 8 | val billingZip: String, 9 | val billingCity: String, 10 | val billingAddress: String, 11 | val billingState: String, 12 | val billingCountry: String 13 | ) 14 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.utils 2 | 3 | internal object Utils { 4 | fun sanitizePhoneNumber(phone: String): String { 5 | return when { 6 | phone.startsWith("0") -> phone.replaceFirst("^0".toRegex(), "254") 7 | phone.startsWith("+") -> phone.replaceFirst("^\\+".toRegex(), "") 8 | else -> phone 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/utils/ViewUtils.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | import android.content.Context 4 | 5 | fun Context.getStatusBarHeight(): Int { 6 | var result = 0 7 | val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") 8 | if (resourceId > 0) { 9 | result = resources.getDimensionPixelSize(resourceId) 10 | } 11 | return result 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/Session.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Session( 8 | @SerialName("sessionId") 9 | val sessionId: String, 10 | 11 | @SerialName("lastUseDate") 12 | val lastUseDate: Long, 13 | 14 | @SerialName("userId") 15 | val userId: Int 16 | ) 17 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/service/CartService.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network.service 2 | 3 | import com.marknjunge.core.data.model.VerifyOrderDto 4 | import com.marknjunge.core.data.model.VerifyOrderResponse 5 | import retrofit2.http.Body 6 | import retrofit2.http.POST 7 | 8 | interface CartService { 9 | @POST("orders/verify") 10 | suspend fun verifyCart(@Body body: VerifyOrderDto): List 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/VerifyOrderItemOptionDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class VerifyOrderItemOptionDto( 8 | @SerialName("choiceId") 9 | val choiceId: Long, 10 | @SerialName("optionId") 11 | val optionId: Long, 12 | @SerialName("optionPrice") 13 | val optionPrice: Double 14 | ) 15 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/PlaceOrderItemDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class PlaceOrderItemDto( 8 | @SerialName("quantity") 9 | val quantity: Int, 10 | 11 | @SerialName("productId") 12 | val productId: Long, 13 | 14 | @SerialName("options") 15 | val options: List 16 | ) 17 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/NetworkCall.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network 2 | 3 | import com.marknjunge.core.data.model.Resource 4 | import com.marknjunge.core.utils.parseException 5 | 6 | @Suppress("TooGenericExceptionCaught") 7 | internal suspend fun call(block: suspend () -> T): Resource { 8 | return try { 9 | Resource.Success(block()) 10 | } catch (e: Exception) { 11 | return parseException(e) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/data/db/DbRepository.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.data.db 2 | 3 | import com.marknkamau.justjava.data.models.AppProduct 4 | import com.marknkamau.justjava.data.models.CartItem 5 | 6 | interface DbRepository { 7 | suspend fun saveItemToCart(product: AppProduct, quantity: Int) 8 | 9 | suspend fun getCartItems(): List 10 | 11 | suspend fun deleteItemFromCart(item: CartItem) 12 | 13 | suspend fun clearCart() 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/SaveAddressDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class SaveAddressDto( 8 | @SerialName("streetAddress") 9 | val streetAddress: String, 10 | 11 | @SerialName("deliveryInstructions") 12 | val deliveryInstructions: String?, 13 | 14 | @SerialName("latLng") 15 | val latLng: String 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/utils/KeyboardUtils.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | import android.content.Context 4 | import android.view.inputmethod.InputMethodManager 5 | import androidx.appcompat.app.AppCompatActivity 6 | 7 | fun AppCompatActivity.hideKeyboard() { 8 | this.currentFocus?.let { 9 | val manager = this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 10 | manager.hideSoftInputFromWindow(it.windowToken, 0) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/data/network/AppFirebaseService.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.data.network 2 | 3 | import com.google.firebase.messaging.FirebaseMessaging 4 | import com.marknjunge.core.data.network.service.FirebaseService 5 | import kotlinx.coroutines.tasks.await 6 | 7 | class AppFirebaseService : FirebaseService { 8 | private val firebaseMessaging = FirebaseMessaging.getInstance() 9 | 10 | override suspend fun getFcmToken(): String = firebaseMessaging.token.await() 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/SignUpDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class SignUpDto( 8 | @SerialName("firstName") 9 | val firstName: String, 10 | 11 | @SerialName("lastName") 12 | val lastName: String, 13 | 14 | @SerialName("password") 15 | val password: String, 16 | 17 | @SerialName("email") 18 | val email: String 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_location_pin.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/UpdateUserDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class UpdateUserDto( 8 | @SerialName("firstName") 9 | val firstName: String, 10 | 11 | @SerialName("lastName") 12 | val lastName: String, 13 | 14 | @SerialName("mobileNumber") 15 | val mobileNumber: String, 16 | 17 | @SerialName("email") 18 | val email: String 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/data/network/GoogleSignInServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.data.network 2 | 3 | import com.google.android.gms.auth.api.signin.GoogleSignInClient 4 | import com.marknjunge.core.data.network.service.GoogleSignInService 5 | import kotlinx.coroutines.tasks.await 6 | 7 | class GoogleSignInServiceImpl(private val googleSignInClient: GoogleSignInClient) : 8 | GoogleSignInService { 9 | override suspend fun signOut() { 10 | googleSignInClient.signOut().await() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_error.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/VerifyOrderItemDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class VerifyOrderItemDto( 8 | @SerialName("index") 9 | val index: Int, 10 | @SerialName("productId") 11 | val productId: Long, 12 | @SerialName("productBasePrice") 13 | val productBasePrice: Double, 14 | @SerialName("options") 15 | val options: List 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/data/db/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.data.db 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.marknkamau.justjava.data.models.CartOptionEntity 6 | import com.marknkamau.justjava.data.models.CartProductEntity 7 | 8 | @Database( 9 | entities = [CartProductEntity::class, CartOptionEntity::class], 10 | version = 2, 11 | exportSchema = false 12 | ) 13 | abstract class AppDatabase : RoomDatabase() { 14 | abstract fun cartDao(): CartDao 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/PlaceOrderDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class PlaceOrderDto( 8 | @SerialName("paymentMethod") 9 | val paymentMethod: String, 10 | 11 | @SerialName("items") 12 | val items: List, 13 | 14 | @SerialName("addressId") 15 | val addressId: Long, 16 | 17 | @SerialName("additionalComments") 18 | val additionalComments: String? = null 19 | ) 20 | -------------------------------------------------------------------------------- /core/src/test/java/com/marknjunge/core/utils/UtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.utils 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | 6 | class UtilsTest { 7 | @Test 8 | fun should_sanitizePhoneNumber() { 9 | val num1 = "0712345678" 10 | val num1Sanitized = Utils.sanitizePhoneNumber(num1) 11 | Assert.assertEquals("254712345678", num1Sanitized) 12 | 13 | val num2 = "+254712345678" 14 | val num2Sanitized = Utils.sanitizePhoneNumber(num2) 15 | Assert.assertEquals("254712345678", num2Sanitized) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/marknkamau/justjava/testUtils/TestApplicationRunner.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.testUtils 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 | @Suppress("unused") 9 | class TestApplicationRunner : AndroidJUnitRunner() { 10 | override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application { 11 | return super.newApplication(cl, HiltTestApplication::class.java.name, context) 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/utils/CurrencyFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | import java.math.RoundingMode 4 | import java.text.NumberFormat 5 | import java.util.* 6 | 7 | object CurrencyFormatter { 8 | fun format(number: Double, decimals: Int = 0): String { 9 | val numberFormat = NumberFormat.getInstance(Locale("en")) 10 | numberFormat.roundingMode = RoundingMode.HALF_UP 11 | numberFormat.maximumFractionDigits = decimals 12 | numberFormat.minimumFractionDigits = decimals 13 | return numberFormat.format(number) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/ProductChoiceOption.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | @Parcelize 9 | @Serializable 10 | data class ProductChoiceOption( 11 | @SerialName("id") 12 | val id: Int, 13 | @SerialName("price") 14 | val price: Double, 15 | 16 | @SerialName("name") 17 | val name: String, 18 | 19 | @SerialName("description") 20 | val description: String? = null 21 | ) : Parcelable 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/Address.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | @Parcelize 9 | @Serializable 10 | data class Address( 11 | @SerialName("id") 12 | val id: Long, 13 | 14 | @SerialName("streetAddress") 15 | val streetAddress: String, 16 | 17 | @SerialName("deliveryInstructions") 18 | val deliveryInstructions: String?, 19 | 20 | @SerialName("latLng") 21 | val latLng: String 22 | ) : Parcelable 23 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/OrderItemOption.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class OrderItemOption( 8 | @SerialName("choiceId") 9 | val choiceId: Int, 10 | 11 | @SerialName("choiceName") 12 | val choiceName: String, 13 | 14 | @SerialName("optionId") 15 | val optionId: Int, 16 | 17 | @SerialName("optionPrice") 18 | val optionPrice: Double, 19 | 20 | @SerialName("id") 21 | val id: Int, 22 | 23 | @SerialName("optionName") 24 | val optionName: String 25 | ) 26 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/service/PaymentsService.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network.service 2 | 3 | import com.marknjunge.core.data.model.InitiateCardPaymentDto 4 | import com.marknjunge.core.data.model.RequestMpesaDto 5 | import com.marknjunge.core.data.model.ApiResponse 6 | import retrofit2.http.Body 7 | import retrofit2.http.POST 8 | 9 | interface PaymentsService { 10 | @POST("/payments/mpesa/request") 11 | suspend fun requestMpesa(@Body body: RequestMpesaDto): ApiResponse 12 | 13 | @POST("/payments/card/initiate") 14 | suspend fun initiateCardPayment(@Body body: InitiateCardPaymentDto): ApiResponse 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/VerifyOrderResponse.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class VerifyOrderResponse( 8 | @SerialName("itemId") 9 | val itemId: Int, 10 | 11 | @SerialName("errorString") 12 | val errorString: String, 13 | 14 | @SerialName("errorType") 15 | val errorType: ErrorType, 16 | 17 | @SerialName("errorModel") 18 | val errorModel: ErrorModel, 19 | 20 | @SerialName("index") 21 | val index: Int, 22 | 23 | @SerialName("newPrice") 24 | val newPrice: Double 25 | ) 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mail.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/OrderItem.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class OrderItem( 8 | 9 | @SerialName("quantity") 10 | val quantity: Int, 11 | 12 | @SerialName("productId") 13 | val productId: Int, 14 | 15 | @SerialName("totalPrice") 16 | val totalPrice: Double, 17 | 18 | @SerialName("productBasePrice") 19 | val productBasePrice: Double, 20 | 21 | @SerialName("productName") 22 | val productName: String, 23 | 24 | @SerialName("options") 25 | val options: List 26 | ) 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.marknkamau.justjava.ui.main.MainActivity 7 | 8 | /** 9 | * Created by MarkNjunge. 10 | * mark.kamau@outlook.com 11 | * https://github.com/MarkNjunge 12 | */ 13 | 14 | class SplashActivity : AppCompatActivity() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | 18 | val intent = Intent(this, MainActivity::class.java) 19 | startActivity(intent) 20 | finish() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/utils/PhoneNumberUtils.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | object PhoneNumberUtils { 4 | fun sanitize(number: String): String { 5 | val num = number.replace(" ", "") 6 | return when { 7 | num.startsWith("0") -> num.replaceFirst("^0".toRegex(), "254") 8 | num.startsWith("+") -> num.replaceFirst("^\\+".toRegex(), "") 9 | else -> number 10 | } 11 | } 12 | 13 | fun beautify(number: String): String { 14 | val num = sanitize(number) 15 | return "+${num.subSequence(0, 3)} ${num.subSequence(3, 6)} ${num.subSequence(6, 9)} ${num.subSequence(9, 12)}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/res/menu/overflow_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 15 | 18 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/repository/ProductsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.repository 2 | 3 | import com.marknjunge.core.data.model.Product 4 | import com.marknjunge.core.data.model.Resource 5 | import com.marknjunge.core.data.network.service.ApiService 6 | import com.marknjunge.core.data.network.call 7 | 8 | interface ProductsRepository { 9 | suspend fun getProducts(): Resource> 10 | } 11 | 12 | class ApiProductsRepository(private val apiService: ApiService) : ProductsRepository { 13 | override suspend fun getProducts(): Resource> { 14 | return call { 15 | apiService.getProducts() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/ProductChoice.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | @Parcelize 9 | @Serializable 10 | data class ProductChoice( 11 | @SerialName("id") 12 | val id: Int, 13 | 14 | @SerialName("name") 15 | val name: String, 16 | 17 | @SerialName("position") 18 | val position: Int, 19 | 20 | @SerialName("qtyMax") 21 | val qtyMax: Int, 22 | 23 | @SerialName("qtyMin") 24 | val qtyMin: Int, 25 | 26 | @SerialName("options") 27 | val options: List 28 | 29 | ) : Parcelable 30 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/service/OrdersService.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network.service 2 | 3 | import com.marknjunge.core.data.model.* 4 | import retrofit2.http.* 5 | 6 | interface OrdersService { 7 | @POST("/orders/place") 8 | suspend fun placeOrder(@Body body: PlaceOrderDto): Order 9 | 10 | @GET("/users/current/orders") 11 | suspend fun getOrders(): List 12 | 13 | @GET("/orders/{id}") 14 | suspend fun getOrderById(@Path("id") id: String): Order 15 | 16 | @POST("/orders/{id}/paymentMethod") 17 | suspend fun changePaymentMethod( 18 | @Path("id") id: String, 19 | @Body changePaymentMethodDto: ChangePaymentMethodDto 20 | ): ApiResponse 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/interceptors/SessionIdInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network.interceptors 2 | 3 | import com.marknjunge.core.data.local.PreferencesRepository 4 | import okhttp3.Interceptor 5 | import okhttp3.Response 6 | 7 | class SessionIdInterceptor(private val preferencesRepository: PreferencesRepository) : Interceptor { 8 | override fun intercept(chain: Interceptor.Chain): Response { 9 | val request = if (preferencesRepository.isSignedIn) { 10 | chain.request().newBuilder().addHeader("session-id", preferencesRepository.sessionId).build() 11 | } else { 12 | chain.request() 13 | } 14 | 15 | return chain.proceed(request) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/di/PreferencesModule.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.di 2 | 3 | import android.content.Context 4 | import com.marknjunge.core.data.local.PreferencesRepository 5 | import com.marknkamau.justjava.data.preferences.PreferencesRepositoryImpl 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.qualifiers.ApplicationContext 10 | import dagger.hilt.components.SingletonComponent 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object PreferencesModule { 15 | @Provides 16 | fun providePreferencesModule(@ApplicationContext context: Context): PreferencesRepository { 17 | return PreferencesRepositoryImpl(context) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.ap_ 3 | 4 | # Files for the Dalvik VM 5 | *.dex 6 | 7 | # Java class files 8 | *.class 9 | 10 | # Generated files 11 | bin/ 12 | gen/ 13 | 14 | # Gradle files 15 | .gradle/ 16 | build/ 17 | 18 | # Local configuration file (sdk path, etc) 19 | local.properties 20 | 21 | # Proguard folder generated by Eclipse 22 | proguard/ 23 | 24 | # Log Files 25 | *.log 26 | 27 | # Android Studio Navigation editor temp files 28 | .navigation/ 29 | 30 | # Android Studio captures folder 31 | captures/ 32 | 33 | .idea/ 34 | *.iml 35 | keystore.properties 36 | fabric.properties 37 | google-services.json 38 | justjavastaff/justjavastaff-release.apk 39 | sync.ffs_db 40 | keys.properties 41 | *.apk 42 | sentry.properties 43 | app/release -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/utils/ItemDiffCallback.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | 5 | class ItemDiffCallback( 6 | private val oldList: MutableList, 7 | private val newList: MutableList 8 | ) : DiffUtil.Callback() { 9 | override fun getOldListSize() = oldList.size 10 | 11 | override fun getNewListSize() = newList.size 12 | 13 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 14 | return newList[newItemPosition]!! == oldList[oldItemPosition] 15 | } 16 | 17 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 18 | return newList[newItemPosition]!! == oldList[oldItemPosition] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/utils/ReleaseTree.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | import io.sentry.Breadcrumb 4 | import io.sentry.Sentry 5 | import io.sentry.SentryLevel 6 | import timber.log.Timber 7 | 8 | class ReleaseTree : Timber.Tree() { 9 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { 10 | val breadcrumb = Breadcrumb().apply { 11 | this.message = message 12 | this.level = when (priority) { 13 | 3 -> SentryLevel.DEBUG 14 | 4 -> SentryLevel.INFO 15 | 5 -> SentryLevel.WARNING 16 | 6 -> SentryLevel.ERROR 17 | else -> SentryLevel.INFO 18 | } 19 | } 20 | Sentry.addBreadcrumb(breadcrumb) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/utils/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | import android.content.Context 4 | import android.util.Patterns 5 | import android.widget.Toast 6 | import java.util.* 7 | 8 | /** 9 | * Created by MarkNjunge. 10 | * mark.kamau@outlook.com 11 | * https://github.com/MarkNjunge 12 | */ 13 | 14 | fun Iterable.replace(old: E, new: E) = map { if (it == old) new else it } 15 | 16 | fun String.isValidEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches() 17 | 18 | fun Context.toast(text: String, duration: Int = Toast.LENGTH_SHORT) = Toast.makeText(this, text, duration).show() 19 | 20 | fun String.capitalize() = lowercase(Locale.getDefault()) 21 | .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/repository/CartRepository.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.repository 2 | 3 | import com.marknjunge.core.data.model.Resource 4 | import com.marknjunge.core.data.model.VerifyOrderDto 5 | import com.marknjunge.core.data.model.VerifyOrderResponse 6 | import com.marknjunge.core.data.network.call 7 | import com.marknjunge.core.data.network.service.CartService 8 | 9 | interface CartRepository { 10 | suspend fun verifyOrder(dto: VerifyOrderDto): Resource> 11 | } 12 | 13 | class ApiCartRepository(private val cartService: CartService) : CartRepository { 14 | override suspend fun verifyOrder(dto: VerifyOrderDto): Resource> { 15 | return call { 16 | cartService.verifyCart(dto) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_orders.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/data/models/CartProductEntity.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.data.models 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.PrimaryKey 7 | import kotlinx.parcelize.Parcelize 8 | 9 | @Entity(tableName = "cart_products") 10 | @Parcelize 11 | data class CartProductEntity( 12 | @PrimaryKey(autoGenerate = true) 13 | val id: Long, 14 | 15 | @ColumnInfo(name = "product_id") 16 | var productId: Long, 17 | 18 | @ColumnInfo(name = "product_name") 19 | var productName: String, 20 | 21 | @ColumnInfo(name = "product_base_price") 22 | var productBasePrice: Double, 23 | 24 | @ColumnInfo(name = "total_price") 25 | var totalPrice: Double, 26 | 27 | @ColumnInfo(name = "quantity") 28 | var quantity: Int 29 | ) : Parcelable 30 | -------------------------------------------------------------------------------- /app/src/test/java/com/marknkamau/justjava/utils/PhoneNumberUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | 6 | class PhoneNumberUtilsTest { 7 | @Test 8 | fun `starts with zero`() { 9 | val number = "0712345678" 10 | val sanitized = PhoneNumberUtils.sanitize(number) 11 | Assert.assertEquals("254712345678", sanitized) 12 | } 13 | 14 | @Test 15 | fun `starts with plus`() { 16 | val number = "+254712345678" 17 | val sanitized = PhoneNumberUtils.sanitize(number) 18 | Assert.assertEquals("254712345678", sanitized) 19 | } 20 | 21 | @Test 22 | fun `can beautify`() { 23 | val number = "254712345678" 24 | val pretty = PhoneNumberUtils.beautify(number) 25 | Assert.assertEquals("+254 712 345 678", pretty) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | org.gradle.parallel=true 18 | org.gradle.caching=true 19 | android.useAndroidX=true 20 | android.enableJetifier=true -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_reset_password.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.marknkamau.justjava.data.db.AppDatabase 6 | import com.marknkamau.justjava.data.db.CartDao 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.android.qualifiers.ApplicationContext 11 | import dagger.hilt.components.SingletonComponent 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | object DatabaseModule { 16 | 17 | @Provides 18 | fun provideDatabase(@ApplicationContext context: Context): AppDatabase { 19 | return Room.databaseBuilder(context, AppDatabase::class.java, "justjava-db") 20 | .fallbackToDestructiveMigrationFrom(1) 21 | .build() 22 | } 23 | 24 | @Provides 25 | fun provideCartDao(appDatabase: AppDatabase): CartDao = appDatabase.cartDao() 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cart_light.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/Product.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | @Parcelize 9 | @Serializable 10 | data class Product( 11 | @SerialName("id") 12 | val id: Long, 13 | 14 | @SerialName("name") 15 | val name: String, 16 | 17 | @SerialName("slug") 18 | val slug: String, 19 | 20 | @SerialName("image") 21 | val image: String, 22 | 23 | @SerialName("createdAt") 24 | val createdAt: Long, 25 | 26 | @SerialName("price") 27 | val price: Double, 28 | 29 | @SerialName("description") 30 | val description: String, 31 | 32 | @SerialName("type") 33 | val type: String, 34 | 35 | @SerialName("choices") 36 | val choices: List?, 37 | 38 | @SerialName("status") 39 | val status: String 40 | ) : Parcelable 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/data/db/CartDao.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.data.db 2 | 3 | import androidx.room.* 4 | import com.marknkamau.justjava.data.models.CartItem 5 | import com.marknkamau.justjava.data.models.CartOptionEntity 6 | import com.marknkamau.justjava.data.models.CartProductEntity 7 | 8 | @Dao 9 | interface CartDao { 10 | @Insert 11 | suspend fun addItem(orderItem: CartProductEntity): Long 12 | 13 | @Insert 14 | suspend fun addItem(option: CartOptionEntity): Long 15 | 16 | @Query("SELECT * FROM cart_products") 17 | suspend fun getAllWithOptions(): List 18 | 19 | @Query("SELECT SUM(total_price) from cart_products") 20 | suspend fun getTotal(): String 21 | 22 | @Delete 23 | suspend fun deleteItem(item: CartProductEntity) 24 | 25 | @Query("DELETE FROM cart_products") 26 | suspend fun deleteAll() 27 | 28 | @Update 29 | suspend fun updateItem(item: CartProductEntity) 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_profile_light.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cart.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/Order.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Order( 8 | 9 | @SerialName("additionalComments") 10 | val additionalComments: String?, 11 | 12 | @SerialName("datePlaced") 13 | val datePlaced: Long, 14 | 15 | @SerialName("totalPrice") 16 | val totalPrice: Double, 17 | 18 | @SerialName("paymentMethod") 19 | val paymentMethod: PaymentMethod, 20 | 21 | @SerialName("id") 22 | val id: String, 23 | 24 | @SerialName("userId") 25 | val userId: Int, 26 | 27 | @SerialName("items") 28 | val items: List, 29 | 30 | @SerialName("paymentStatus") 31 | val paymentStatus: PaymentStatus, 32 | 33 | @SerialName("status") 34 | val status: OrderStatus, 35 | 36 | @SerialName("addressId") 37 | val addressId: Long? = null 38 | ) 39 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/User.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | @Parcelize 9 | @Serializable 10 | data class User( 11 | @SerialName("id") 12 | val id: Int, 13 | 14 | @SerialName("firstName") 15 | val firstName: String, 16 | 17 | @SerialName("lastName") 18 | val lastName: String, 19 | 20 | @SerialName("createdAt") 21 | val createdAt: Long, 22 | 23 | @SerialName("mobileNumber") 24 | val mobileNumber: String?, 25 | 26 | @SerialName("email") 27 | val email: String, 28 | 29 | @SerialName("fcmToken") 30 | val fcmToken: String?, 31 | 32 | // TODO Use enum 33 | @SerialName("signInMethod") 34 | val signInMethod: String, 35 | 36 | @SerialName("addresses") 37 | val address: List
38 | ) : Parcelable 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/productDetails/ProductDetailsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.productDetails 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.marknkamau.justjava.data.db.DbRepository 8 | import com.marknkamau.justjava.data.models.AppProduct 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | 13 | @HiltViewModel 14 | class ProductDetailsViewModel @Inject constructor(private val dbRepository: DbRepository) : ViewModel() { 15 | fun addItemToCart(item: AppProduct, quantity: Int): LiveData { 16 | val livedata = MutableLiveData() 17 | 18 | viewModelScope.launch { 19 | livedata.value = dbRepository.saveItemToCart(item, quantity) 20 | } 21 | 22 | return livedata 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/model/InitiateCardPaymentDto.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class InitiateCardPaymentDto( 8 | @SerialName("orderId") 9 | var orderId: String, 10 | 11 | @SerialName("cardNo") 12 | var cardNo: String, 13 | 14 | @SerialName("cvv") 15 | var cvv: String, 16 | 17 | @SerialName("expiryMonth") 18 | var expiryMonth: String, 19 | 20 | @SerialName("expiryYear") 21 | var expiryYear: String, 22 | 23 | @SerialName("billingZip") 24 | var billingZip: String, 25 | 26 | @SerialName("billingCity") 27 | var billingCity: String, 28 | 29 | @SerialName("billingAddress") 30 | var billingAddress: String, 31 | 32 | @SerialName("billingState") 33 | var billingState: String, 34 | 35 | @SerialName("billingCountry") 36 | var billingCountry: String 37 | ) 38 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/service/UsersService.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network.service 2 | 3 | import com.marknjunge.core.data.model.* 4 | import com.marknjunge.core.data.model.SaveAddressDto 5 | import com.marknjunge.core.data.model.UpdateFcmTokenDto 6 | import com.marknjunge.core.data.model.UpdateUserDto 7 | import retrofit2.http.* 8 | 9 | interface UsersService { 10 | @GET("users/current") 11 | suspend fun getCurrentUser(): User 12 | 13 | @PATCH("users/current") 14 | suspend fun updateUser(@Body body: UpdateUserDto) 15 | 16 | @PATCH("users/current/fcm") 17 | suspend fun updateFcmToken(@Body body: UpdateFcmTokenDto) 18 | 19 | @POST("users/current/addresses") 20 | suspend fun saveAddress(@Body body: SaveAddressDto): Address 21 | 22 | @DELETE("users/current/addresses/{id}") 23 | suspend fun deleteAddress(@Path("id") addressId: Long) 24 | 25 | @DELETE("users/current") 26 | suspend fun deleteUser() 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/utils/serialization/PaymentMethodSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.utils.serialization 2 | 3 | import com.marknjunge.core.data.model.PaymentMethod 4 | import kotlinx.serialization.* 5 | import kotlinx.serialization.descriptors.PrimitiveKind 6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 7 | import kotlinx.serialization.descriptors.SerialDescriptor 8 | import kotlinx.serialization.encoding.Decoder 9 | import kotlinx.serialization.encoding.Encoder 10 | 11 | object PaymentMethodSerializer : KSerializer { 12 | override val descriptor: SerialDescriptor = 13 | PrimitiveSerialDescriptor("paymentMethodSerializer", PrimitiveKind.STRING) 14 | 15 | override fun deserialize(decoder: Decoder): PaymentMethod { 16 | return PaymentMethod.valueOf(decoder.decodeString()) 17 | } 18 | 19 | override fun serialize(encoder: Encoder, value: PaymentMethod) { 20 | encoder.encodeString(value.s) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/utils/EditTextExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | import android.text.Editable 4 | import android.text.TextWatcher 5 | import android.widget.EditText 6 | import com.google.android.material.textfield.TextInputLayout 7 | 8 | fun EditText.onTextChanged(onChange: () -> Unit) { 9 | this.addTextChangedListener(object : TextWatcher { 10 | override fun afterTextChanged(s: Editable?) { 11 | // Do nothing 12 | } 13 | 14 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { 15 | // Do nothing 16 | } 17 | 18 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 19 | onChange() 20 | } 21 | }) 22 | } 23 | 24 | val EditText.trimmedText: String 25 | get() = this.text.trim().toString() 26 | 27 | fun TextInputLayout.resetErrorOnChange(editText: EditText) { 28 | editText.onTextChanged { this.error = null } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/interceptors/NetworkConnectionInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network.interceptors 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import com.marknjunge.core.utils.NoInternetException 6 | import okhttp3.Interceptor 7 | import okhttp3.Response 8 | 9 | internal class NetworkConnectionInterceptor(private val context: Context) : Interceptor { 10 | override fun intercept(chain: Interceptor.Chain): Response { 11 | 12 | if (!isInternetAvailable()) { 13 | throw NoInternetException() 14 | } 15 | 16 | return chain.proceed(chain.request()) 17 | } 18 | 19 | private fun isInternetAvailable(): Boolean { 20 | val connectivityManager = 21 | context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 22 | 23 | connectivityManager.activeNetworkInfo.also { 24 | return it != null 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/service/AuthService.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network.service 2 | 3 | import com.marknjunge.core.data.model.* 4 | import com.marknjunge.core.data.model.SignInGoogleDto 5 | import com.marknjunge.core.data.model.SignInResponse 6 | import retrofit2.http.Body 7 | import retrofit2.http.DELETE 8 | import retrofit2.http.POST 9 | 10 | interface AuthService { 11 | @POST("auth/google") 12 | suspend fun signInWithGoogle(@Body body: SignInGoogleDto): SignInResponse 13 | 14 | @POST("auth/signup") 15 | suspend fun signUp(@Body body: SignUpDto): SignInResponse 16 | 17 | @POST("auth/signin") 18 | suspend fun signIn(@Body body: SignInDto): SignInResponse 19 | 20 | @DELETE("auth/signout") 21 | suspend fun signOut() 22 | 23 | @POST("auth/requestPasswordReset") 24 | suspend fun requestPasswordReset(@Body body: RequestPasswordResetDto): ApiResponse 25 | 26 | @POST("auth/resetPassword") 27 | suspend fun resetPassword(@Body body: ResetPasswordDto): ApiResponse 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_product_choice_option.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 19 | 20 | 26 | -------------------------------------------------------------------------------- /core/src/test/java/com/marknjunge/core/SampleData.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core 2 | 3 | import com.marknjunge.core.data.model.Address 4 | import com.marknjunge.core.data.model.Session 5 | import com.marknjunge.core.data.model.User 6 | import com.marknjunge.core.data.model.Product 7 | import com.marknjunge.core.data.model.ProductChoice 8 | import com.marknjunge.core.data.model.ProductChoiceOption 9 | 10 | internal object SampleData { 11 | val address = Address(0, "Street", "instructions", "-1,1") 12 | val user = User(1, "fName", "lName", 0L, "254712345678", "contact@mail.com", "token", "PASSWORD", listOf(address)) 13 | val session = Session("", 0L, 0) 14 | 15 | val productChoiceOption = ProductChoiceOption(0, 0.0, "name", "desc") 16 | val productChoice = ProductChoice(0, "choice", 0, 0, 0, listOf(productChoiceOption)) 17 | val product = Product( 18 | 0, 19 | "prod", 20 | "prod", 21 | "image", 22 | 0L, 23 | 0.0, 24 | "desc", 25 | "type", 26 | listOf(productChoice), 27 | "status" 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_address_book.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_website.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/orders/OrdersViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.orders 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.marknjunge.core.data.model.Order 8 | import com.marknjunge.core.data.model.Resource 9 | import com.marknjunge.core.data.repository.OrdersRepository 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class OrdersViewModel @Inject constructor(private val ordersRepository: OrdersRepository) : ViewModel() { 16 | private val _loading = MutableLiveData() 17 | val loading: LiveData = _loading 18 | 19 | private val _orders = MutableLiveData>>() 20 | val orders: LiveData>> = _orders 21 | 22 | fun getOrders() { 23 | viewModelScope.launch { 24 | _loading.value = true 25 | _orders.value = ordersRepository.getOrders() 26 | _loading.value = false 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/marknkamau/justjava/testUtils/EspressoUtils.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.testUtils 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import androidx.test.espresso.Espresso 5 | import androidx.test.espresso.ViewInteraction 6 | import androidx.test.espresso.action.ViewActions 7 | import androidx.test.espresso.assertion.ViewAssertions 8 | import androidx.test.espresso.contrib.RecyclerViewActions 9 | import androidx.test.espresso.matcher.ViewMatchers 10 | 11 | fun onViewWithId(id: Int): ViewInteraction = Espresso.onView(ViewMatchers.withId(id)) 12 | 13 | fun onViewWithText(text: String): ViewInteraction = Espresso.onView(ViewMatchers.withText(text)) 14 | 15 | fun ViewInteraction.click(): ViewInteraction = perform(ViewActions.click()) 16 | 17 | fun ViewInteraction.clickRecyclerViewItem(position: Int): ViewInteraction = perform( 18 | RecyclerViewActions.actionOnItemAtPosition(position, ViewActions.click()) 19 | ) 20 | 21 | fun ViewInteraction.scrollTo(): ViewInteraction = perform(ViewActions.scrollTo()) 22 | 23 | fun ViewInteraction.isDisplayed(): ViewInteraction = check(ViewAssertions.matches(ViewMatchers.isDisplayed())) -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/data/models/CartOptionEntity.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.data.models 2 | 3 | import android.os.Parcelable 4 | import androidx.room.* 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Entity( 8 | tableName = "cart_product_options", 9 | foreignKeys = [ForeignKey( 10 | entity = CartProductEntity::class, 11 | parentColumns = arrayOf("id"), 12 | childColumns = arrayOf("cart_products_row_id"), 13 | onDelete = ForeignKey.CASCADE 14 | )], 15 | indices = [Index(value = ["cart_products_row_id"])] 16 | ) 17 | @Parcelize 18 | data class CartOptionEntity( 19 | @PrimaryKey(autoGenerate = true) 20 | var id: Long, 21 | 22 | @ColumnInfo(name = "choice_id") 23 | var choiceId: Long, 24 | 25 | @ColumnInfo(name = "choice_name") 26 | var choiceName: String, 27 | 28 | @ColumnInfo(name = "option_id") 29 | var optionId: Long, 30 | 31 | @ColumnInfo(name = "option_name") 32 | var optionName: String, 33 | 34 | @ColumnInfo(name = "option_price") 35 | var optionPrice: Double, 36 | 37 | @ColumnInfo(name = "cart_products_row_id") 38 | var cartProductsRowId: Long 39 | ) : Parcelable 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.main 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.marknjunge.core.data.model.Product 8 | import com.marknjunge.core.data.model.Resource 9 | import com.marknjunge.core.data.repository.ProductsRepository 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class MainViewModel @Inject constructor (private val productsRepository: ProductsRepository) : ViewModel() { 16 | private val _loading = MutableLiveData() 17 | val loading: LiveData = _loading 18 | 19 | private val _products = MutableLiveData>>() 20 | val products: LiveData>> = _products 21 | 22 | fun getProducts() { 23 | viewModelScope.launch { 24 | _loading.value = true 25 | _products.value = productsRepository.getProducts() 26 | _loading.value = false 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #946D66 4 | #4F332E 5 | #3E2723 6 | #F57C00 7 | #212121 8 | #757575 9 | #E0E0E0 10 | #CCCCCC 11 | #888888 12 | #E53E3E 13 | 14 | #333333 15 | #DEDEDE 16 | #D8801C 17 | #F9EBDA 18 | #28549F 19 | #DCE3F0 20 | #2D5D36 21 | #DDE5DE 22 | #A51D1D 23 | #F1DADA 24 | 25 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/utils/ExceptionUtils.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.utils 2 | 3 | import com.marknjunge.core.data.model.Resource 4 | import com.marknjunge.core.data.model.ApiResponse 5 | import kotlinx.serialization.SerializationException 6 | import retrofit2.HttpException 7 | import timber.log.Timber 8 | 9 | internal fun parseException(e: Exception): Resource { 10 | Timber.e(e) 11 | val genericErrorMessage = "Something went wrong. Please try again." 12 | 13 | return when (e) { 14 | is HttpException -> { 15 | e.response()?.errorBody()?.string()?.let { errorString -> 16 | try { 17 | val apiResponse = appJsonConfig.decodeFromString(ApiResponse.serializer(), errorString) 18 | Timber.e("${e.response()?.code()}, $errorString") 19 | Resource.Failure(apiResponse) 20 | } catch (e: SerializationException) { 21 | Resource.Failure(ApiResponse(errorString)) 22 | } 23 | } ?: Resource.Failure(ApiResponse(genericErrorMessage)) 24 | } 25 | else -> { 26 | Resource.Failure(ApiResponse(e.message ?: genericErrorMessage)) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/data/preferences/PreferencesRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.data.preferences 2 | 3 | import android.content.Context 4 | import com.marknjunge.core.data.local.PreferencesRepository 5 | import com.marknjunge.core.data.model.User 6 | import com.marknjunge.core.utils.appJsonConfig 7 | import com.marknkamau.justjava.utils.PreferenceUtils 8 | 9 | class PreferencesRepositoryImpl(private val context: Context) : PreferencesRepository { 10 | companion object { 11 | const val USER_KEY = "user" 12 | const val SESSION_ID_KEY = "session_id" 13 | } 14 | 15 | private val prefUtils by lazy { 16 | PreferenceUtils( 17 | context.getSharedPreferences("justjava_prefs", Context.MODE_PRIVATE), 18 | appJsonConfig 19 | ) 20 | } 21 | 22 | override val isSignedIn: Boolean 23 | get() = prefUtils.getObject(USER_KEY, User.serializer()) != null 24 | 25 | override var sessionId: String 26 | get() = prefUtils.get(SESSION_ID_KEY) 27 | set(value) = prefUtils.set(SESSION_ID_KEY, value) 28 | 29 | override var user: User? 30 | get() = prefUtils.getObject(USER_KEY, User.serializer())!! 31 | set(value) = prefUtils.setObject(USER_KEY, value, User.serializer()) 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/interceptors/ConvertNoContentInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network.interceptors 2 | 3 | import com.marknjunge.core.data.model.ApiResponse 4 | import com.marknjunge.core.utils.appJsonConfig 5 | import okhttp3.Interceptor 6 | import okhttp3.MediaType.Companion.toMediaType 7 | import okhttp3.Protocol 8 | import okhttp3.Response 9 | import okhttp3.ResponseBody.Companion.toResponseBody 10 | 11 | class ConvertNoContentInterceptor : Interceptor { 12 | private val mediaType = "application/json".toMediaType() 13 | 14 | override fun intercept(chain: Interceptor.Chain): Response { 15 | val response = chain.proceed(chain.request()) 16 | 17 | return if (response.code == 204 || response.body?.contentLength() == 0L) { 18 | 19 | val apiResponse = ApiResponse("No content") 20 | val rawBody = appJsonConfig.encodeToString(ApiResponse.serializer(), apiResponse) 21 | 22 | Response.Builder() 23 | .code(200) 24 | .protocol(Protocol.HTTP_1_1) 25 | .body(rawBody.toResponseBody(mediaType)) 26 | .request(chain.request()) 27 | .message("") 28 | .build() 29 | } else { 30 | response 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /keystore/justjava-debug.txt: -------------------------------------------------------------------------------- 1 | File: justjava-debug.jks 2 | Password: justjava-debug 3 | 4 | keytool -list -v -keystore justjava-debug -alias key0 -storepass justjava-debug -keypass justjava-debug 5 | 6 | ====================================== 7 | ========== KEYSTORE DETAILS ========== 8 | ====================================== 9 | 10 | Alias name: key0 11 | Creation date: Nov 8, 2019 12 | Entry type: PrivateKeyEntry 13 | Certificate chain length: 1 14 | Certificate[1]: 15 | Owner: CN=JustJava, OU=JustJava, O=JustJava, L=Nairobi, ST=Nairobi, C=KE 16 | Issuer: CN=JustJava, OU=JustJava, O=JustJava, L=Nairobi, ST=Nairobi, C=KE 17 | Serial number: 69f77420 18 | Valid from: Fri Nov 08 18:06:04 EAT 2019 until: Tue Nov 01 18:06:04 EAT 2044 19 | Certificate fingerprints: 20 | MD5: 21:7D:CE:3B:96:5C:E0:53:FF:6F:6B:5D:90:C9:7F:DF 21 | SHA1: 38:A2:0A:A1:73:79:57:70:00:A9:F7:55:26:35:23:B3:3E:43:1E:BA 22 | SHA256: 39:0F:CC:5E:AF:AF:76:C2:47:08:3F:16:1C:54:89:FA:89:90:98:2F:D3:61:F2:89:44:85:E6:36:61:64:DF:39 23 | Signature algorithm name: SHA256withRSA 24 | Subject Public Key Algorithm: 2048-bit RSA key 25 | Version: 3 26 | 27 | Extensions: 28 | 29 | #1: ObjectId: 2.5.29.14 Criticality=false 30 | SubjectKeyIdentifier [ 31 | KeyIdentifier [ 32 | 0000: CF AC 55 AC F7 CF 5D E7 E9 40 6C EE AE E5 AA BB ..U...]..@l..... 33 | 0010: 7E 8E 26 F1 ..&. 34 | ] 35 | ] -------------------------------------------------------------------------------- /app/src/test/java/com/marknkamau/justjava/utils/SampleData.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | import com.marknjunge.core.data.model.* 4 | import com.marknkamau.justjava.data.models.CartItem 5 | import com.marknkamau.justjava.data.models.CartOptionEntity 6 | import com.marknkamau.justjava.data.models.CartProductEntity 7 | 8 | internal object SampleData { 9 | val address = Address(0, "Street", "instructions", "-1,1") 10 | val user = User(1, "fName", "lName", 0L, "254712345678", "contact@mail.com", "token", "PASSWORD", listOf(address)) 11 | 12 | val cartOptionEntity = CartOptionEntity(0, 0, "Choice", 0, "Option", 20.0, 0) 13 | val cartItem = CartItem(CartProductEntity(0L, 0L, "Product", 100.0, 100.0, 1), listOf(cartOptionEntity)) 14 | val cartItems = listOf(cartItem) 15 | 16 | val verifyOrderItemOptionDto = VerifyOrderItemOptionDto(0, 0, 100.0) 17 | val verifyOrderItemDto = VerifyOrderItemDto(0, 0, 100.0, listOf(verifyOrderItemOptionDto)) 18 | val verifyOrderDto = VerifyOrderDto(listOf(verifyOrderItemDto)) 19 | 20 | val verifyOrderResponse = VerifyOrderResponse(0, "error", ErrorType.MISSING, ErrorModel.CHOICE, 0, 200.0) 21 | 22 | val orderItem = OrderItem(1, 0, 100.0, 100.0, "Product", listOf(OrderItemOption(0, "Choice", 0, 10.0, 0, "Option"))) 23 | val order = Order(null, 0, 100.0, 24 | PaymentMethod.MPESA, "abc123", 0, listOf(orderItem), PaymentStatus.PAID, OrderStatus.PENDING, 0) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/payMpesa/PayMpesaViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.payMpesa 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.marknjunge.core.data.local.PreferencesRepository 8 | import com.marknjunge.core.data.model.ApiResponse 9 | import com.marknjunge.core.data.model.Resource 10 | import com.marknjunge.core.data.repository.PaymentsRepository 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class PayMpesaViewModel @Inject constructor( 17 | private val preferencesRepository: PreferencesRepository, 18 | private val paymentsRepository: PaymentsRepository 19 | ) : ViewModel() { 20 | 21 | private val _loading = MutableLiveData() 22 | val loading: LiveData = _loading 23 | 24 | fun getUser() = preferencesRepository.user!! 25 | 26 | fun payMpesa(mobileNumber: String, orderId: String): LiveData> { 27 | val livedata = MutableLiveData>() 28 | 29 | viewModelScope.launch { 30 | _loading.value = true 31 | livedata.value = paymentsRepository.requestMpesa(mobileNumber, orderId) 32 | _loading.value = false 33 | } 34 | 35 | return livedata 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_linkedin.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 14 | 19 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_product_choice.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 27 | 28 | 35 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/repository/OrdersRepository.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.repository 2 | 3 | import com.marknjunge.core.data.model.* 4 | import com.marknjunge.core.data.network.call 5 | import com.marknjunge.core.data.network.service.OrdersService 6 | 7 | interface OrdersRepository { 8 | suspend fun placeOrder(dto: PlaceOrderDto): Resource 9 | 10 | suspend fun getOrders(): Resource> 11 | 12 | suspend fun changePaymentMethod(id: String, method: PaymentMethod): Resource 13 | 14 | suspend fun getOrderById(id: String): Resource 15 | } 16 | 17 | class ApiOrdersRepository(private val ordersService: OrdersService) : OrdersRepository { 18 | override suspend fun placeOrder(dto: PlaceOrderDto): Resource { 19 | return call { 20 | ordersService.placeOrder(dto) 21 | } 22 | } 23 | 24 | override suspend fun getOrders(): Resource> { 25 | return call { 26 | ordersService.getOrders() 27 | } 28 | } 29 | 30 | override suspend fun changePaymentMethod(id: String, method: PaymentMethod): Resource { 31 | return call { 32 | val dto = ChangePaymentMethodDto(method) 33 | ordersService.changePaymentMethod(id, dto) 34 | } 35 | } 36 | 37 | override suspend fun getOrderById(id: String): Resource { 38 | return call { 39 | ordersService.getOrderById(id) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/signup/SignUpViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.signup 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.marknjunge.core.data.model.Resource 8 | import com.marknjunge.core.data.model.User 9 | import com.marknjunge.core.data.repository.AuthRepository 10 | import com.marknjunge.core.data.repository.UsersRepository 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class SignUpViewModel @Inject constructor( 17 | private val authRepository: AuthRepository, 18 | private val usersRepository: UsersRepository 19 | ) : ViewModel() { 20 | private val _loading = MutableLiveData() 21 | val loading: LiveData = _loading 22 | 23 | fun signUp( 24 | firstName: String, 25 | lastName: String, 26 | email: String, 27 | password: String 28 | ): LiveData> { 29 | val livedata = MutableLiveData>() 30 | 31 | viewModelScope.launch { 32 | _loading.value = true 33 | livedata.value = authRepository.signUp(firstName, lastName, email, password) 34 | 35 | if (livedata.value is Resource.Success) { 36 | usersRepository.updateFcmToken() 37 | } 38 | 39 | _loading.value = false 40 | } 41 | 42 | return livedata 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.base 2 | 3 | import android.content.Intent 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.core.app.TaskStackBuilder 6 | import androidx.lifecycle.lifecycleScope 7 | import com.marknjunge.core.data.model.Resource 8 | import com.marknjunge.core.data.repository.AuthRepository 9 | import com.marknkamau.justjava.ui.main.MainActivity 10 | import com.marknkamau.justjava.utils.toast 11 | import dagger.hilt.android.AndroidEntryPoint 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | @AndroidEntryPoint 16 | abstract class BaseActivity : AppCompatActivity() { 17 | @Inject lateinit var authRepository: AuthRepository 18 | protected open var requiresSignedIn = false 19 | 20 | protected fun handleApiError(resource: Resource.Failure) { 21 | if (resource.response.message == "Invalid session-id") { 22 | lifecycleScope.launch { 23 | authRepository.signOutLocally() 24 | toast("You have been signed out") 25 | if (requiresSignedIn) { 26 | goToMainActivityNoStack() 27 | } 28 | } 29 | } else { 30 | toast(resource.response.message) 31 | } 32 | } 33 | 34 | private fun goToMainActivityNoStack() { 35 | val intent = Intent(this, MainActivity::class.java) 36 | TaskStackBuilder.create(this) 37 | .addNextIntentWithParentStack(intent) 38 | .startActivities() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/src/test/java/com/marknjunge/core/data/repository/ApiProductsRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.repository 2 | 3 | import com.marknjunge.core.SampleData 4 | import com.marknjunge.core.data.model.Resource 5 | import com.marknjunge.core.data.network.service.ApiService 6 | import io.mockk.MockKAnnotations 7 | import io.mockk.coEvery 8 | import io.mockk.coVerify 9 | import io.mockk.impl.annotations.MockK 10 | import kotlinx.coroutines.runBlocking 11 | import org.junit.Assert 12 | import org.junit.Before 13 | import org.junit.Test 14 | import java.lang.Exception 15 | 16 | class ApiProductsRepositoryTest { 17 | 18 | @MockK 19 | private lateinit var apiService: ApiService 20 | 21 | private lateinit var repo: ProductsRepository 22 | 23 | @Before 24 | fun setup() { 25 | MockKAnnotations.init(this, relaxUnitFun = true) 26 | repo = ApiProductsRepository(apiService) 27 | } 28 | 29 | @Test 30 | fun `verify getProducts runs`() = runBlocking { 31 | coEvery { apiService.getProducts() } returns listOf(SampleData.product) 32 | 33 | val resource = repo.getProducts() 34 | 35 | coVerify { apiService.getProducts() } 36 | Assert.assertTrue(resource is Resource.Success) 37 | } 38 | 39 | @Test 40 | fun `verify getProducts handles error`() = runBlocking { 41 | coEvery { apiService.getProducts() } throws Exception("error") 42 | 43 | val resource = repo.getProducts() 44 | 45 | coVerify { apiService.getProducts() } 46 | Assert.assertTrue(resource is Resource.Failure) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/test/java/com/marknjunge/core/data/repository/ApiCartRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.repository 2 | 3 | import com.marknjunge.core.data.model.Resource 4 | import com.marknjunge.core.data.model.VerifyOrderDto 5 | import com.marknjunge.core.data.network.service.CartService 6 | import io.mockk.MockKAnnotations 7 | import io.mockk.coEvery 8 | import io.mockk.coVerify 9 | import io.mockk.impl.annotations.MockK 10 | import kotlinx.coroutines.runBlocking 11 | import org.junit.Assert 12 | import org.junit.Before 13 | import org.junit.Test 14 | 15 | class ApiCartRepositoryTest { 16 | 17 | @MockK 18 | private lateinit var cartService: CartService 19 | 20 | private lateinit var repo: ApiCartRepository 21 | 22 | @Before 23 | fun setup() { 24 | MockKAnnotations.init(this, relaxUnitFun = true) 25 | repo = ApiCartRepository(cartService) 26 | } 27 | 28 | @Test 29 | fun `verify verifyOrder runs`() = runBlocking { 30 | coEvery { cartService.verifyCart(any()) } returns listOf() 31 | 32 | val resource = repo.verifyOrder(VerifyOrderDto(listOf())) 33 | 34 | coVerify { cartService.verifyCart(any()) } 35 | Assert.assertTrue(resource is Resource.Success) 36 | } 37 | 38 | @Test 39 | fun `verify verifyOrder handles error`() = runBlocking { 40 | coEvery { cartService.verifyCart(any()) } throws Exception("Error") 41 | 42 | val resource = repo.verifyOrder(VerifyOrderDto(listOf())) 43 | 44 | coVerify { cartService.verifyCart(any()) } 45 | Assert.assertTrue(resource is Resource.Failure) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/repository/PaymentRepository.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.repository 2 | 3 | import com.marknjunge.core.data.model.* 4 | import com.marknjunge.core.data.network.call 5 | import com.marknjunge.core.data.network.service.PaymentsService 6 | 7 | interface PaymentsRepository { 8 | suspend fun requestMpesa(mobileNumber: String, orderId: String): Resource 9 | 10 | suspend fun initiateCardPayment(orderId: String, cardDetails: CardDetails): Resource 11 | } 12 | 13 | class ApiPaymentsRepository(private val paymentsService: PaymentsService) : PaymentsRepository { 14 | override suspend fun requestMpesa(mobileNumber: String, orderId: String): Resource { 15 | return call { 16 | paymentsService.requestMpesa(RequestMpesaDto(mobileNumber, orderId)) 17 | } 18 | } 19 | 20 | override suspend fun initiateCardPayment(orderId: String, cardDetails: CardDetails): Resource { 21 | return call { 22 | paymentsService.initiateCardPayment( 23 | InitiateCardPaymentDto( 24 | orderId, 25 | cardDetails.cardNo, 26 | cardDetails.cvv, 27 | cardDetails.expiryMonth, 28 | cardDetails.expiryYear, 29 | cardDetails.billingZip, 30 | cardDetails.billingCity, 31 | cardDetails.billingAddress, 32 | cardDetails.billingState, 33 | cardDetails.billingCountry 34 | ) 35 | ) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_product_details.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | 18 | 19 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_address.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 22 | 23 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/payCard/PayCardViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.payCard 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.marknjunge.core.data.model.ApiResponse 8 | import com.marknjunge.core.data.model.CardDetails 9 | import com.marknjunge.core.data.model.Resource 10 | import com.marknjunge.core.data.repository.PaymentsRepository 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class PayCardViewModel @Inject constructor( 17 | private val paymentsRepository: PaymentsRepository 18 | ) : ViewModel() { 19 | private val _loading = MutableLiveData() 20 | val loading: LiveData = _loading 21 | 22 | fun initiateCardPayment( 23 | orderId: String, 24 | cardNo: String, 25 | expiryMonth: String, 26 | expiryYear: String, 27 | cvv: String 28 | ): LiveData> { 29 | val livedata = MutableLiveData>() 30 | 31 | viewModelScope.launch { 32 | _loading.value = true 33 | livedata.value = paymentsRepository.initiateCardPayment( 34 | orderId, 35 | CardDetails( 36 | cardNo, 37 | cvv, 38 | expiryMonth, 39 | expiryYear, 40 | "07205", 41 | "Hillside", 42 | "470 Mundet PI", 43 | "NJ", 44 | "US" 45 | ) 46 | ) 47 | _loading.value = false 48 | } 49 | 50 | return livedata 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/marknkamau/justjava/testUtils/SampleData.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.testUtils 2 | 3 | import com.marknjunge.core.data.model.* 4 | import com.marknkamau.justjava.data.models.CartItem 5 | import com.marknkamau.justjava.data.models.CartOptionEntity 6 | import com.marknkamau.justjava.data.models.CartProductEntity 7 | 8 | object SampleData { 9 | val address = Address(0, "Street", "instructions", "-1,1") 10 | val user = User(1, "fName", "lName", 0L, "254712345678", "contact@mail.com", "token", "PASSWORD", listOf(address)) 11 | 12 | val productChoiceOption = ProductChoiceOption(0, 0.0, "Single", "A single shot of coffee") 13 | val productChoice = ProductChoice(0, "Single, double or triple", 0, 1, 0, listOf(productChoiceOption)) 14 | val product = Product( 15 | 1, 16 | "Americano", 17 | "americano", 18 | "https://res.cloudinary.com/marknjunge/justjava/products/americano.jpg", 19 | 1574605132, 20 | 120.0, 21 | "Italian espresso gets the American treatment; hot water fills the cup for a rich alternative to drip coffee.", 22 | "coffee", 23 | listOf(productChoice), 24 | "enabled" 25 | ) 26 | 27 | val cartOptionEntity = CartOptionEntity(0, 0, "Single, double or triple", 0, "Single", 20.0, 0) 28 | val cartItem = CartItem(CartProductEntity(0L, 0L, "Americano", 120.0, 120.0, 1), listOf(cartOptionEntity)) 29 | val cartItems = listOf(cartItem) 30 | 31 | val verifyOrderResponse = VerifyOrderResponse(0, "error", ErrorType.MISSING, ErrorModel.CHOICE, 0, 200.0) 32 | 33 | val orderItem = OrderItem(1, 1, 120.0, 120.0, "Americano", listOf(OrderItemOption(1, "Single, double or triple", 3, 0.0, 44, "Single"))) 34 | val order = 35 | Order(null, 1579505466, 120.0, PaymentMethod.MPESA, "AGV7OBST", 1, listOf(orderItem), PaymentStatus.PAID, OrderStatus.PENDING, 0) 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/addressBook/AddressAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.addressBook 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.marknjunge.core.data.model.Address 8 | import com.marknkamau.justjava.databinding.ItemAddressBinding 9 | 10 | class AddressAdapter( 11 | private val deleteAddress: (address: Address) -> Unit 12 | ) : RecyclerView.Adapter() { 13 | 14 | val items = mutableListOf
() 15 | 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 17 | val binding = ItemAddressBinding.inflate(LayoutInflater.from(parent.context), parent, false) 18 | return ViewHolder(binding, deleteAddress) 19 | } 20 | 21 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 22 | holder.bind(items[position]) 23 | } 24 | 25 | override fun getItemCount() = items.size 26 | 27 | fun setItems(newItems: List
) { 28 | items.clear() 29 | items.addAll(newItems) 30 | notifyDataSetChanged() 31 | } 32 | 33 | inner class ViewHolder( 34 | private val binding: ItemAddressBinding, 35 | private val deleteAddress: (Address) -> Unit 36 | ) : RecyclerView.ViewHolder(binding.root) { 37 | fun bind(address: Address) { 38 | binding.tvStreetAddress.text = address.streetAddress 39 | binding.tvDeliveryInstructions.text = address.deliveryInstructions 40 | binding.tvDeliveryInstructions.visibility = 41 | if (address.deliveryInstructions == null) View.GONE else View.VISIBLE 42 | 43 | binding.root.setOnLongClickListener { 44 | deleteAddress(address) 45 | true 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/orderDetail/OrderDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.orderDetail 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.marknjunge.core.data.local.PreferencesRepository 8 | import com.marknjunge.core.data.model.ApiResponse 9 | import com.marknjunge.core.data.model.Order 10 | import com.marknjunge.core.data.model.PaymentMethod 11 | import com.marknjunge.core.data.model.Resource 12 | import com.marknjunge.core.data.repository.OrdersRepository 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.launch 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class OrderDetailViewModel @Inject constructor( 19 | private val ordersRepository: OrdersRepository, 20 | private val preferencesRepository: PreferencesRepository 21 | ) : ViewModel() { 22 | private val _loading = MutableLiveData() 23 | val loading: LiveData = _loading 24 | 25 | private val _order = MutableLiveData>() 26 | val order: LiveData> = _order 27 | 28 | fun getOrder(id: String) { 29 | viewModelScope.launch { 30 | _loading.value = true 31 | _order.value = ordersRepository.getOrderById(id) 32 | _loading.value = false 33 | } 34 | } 35 | 36 | fun changePaymentMethod(id: String, method: PaymentMethod): LiveData> { 37 | val livedata = MutableLiveData>() 38 | viewModelScope.launch { 39 | _loading.value = true 40 | livedata.value = ordersRepository.changePaymentMethod(id, method) 41 | _loading.value = false 42 | } 43 | 44 | return livedata 45 | } 46 | 47 | fun getUser() = preferencesRepository.user!! 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_main_activity_shimmer.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 31 | 32 | 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/data/db/DbRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.data.db 2 | 3 | import com.marknkamau.justjava.data.models.AppProduct 4 | import com.marknkamau.justjava.data.models.CartItem 5 | import com.marknkamau.justjava.data.models.CartOptionEntity 6 | import com.marknkamau.justjava.data.models.CartProductEntity 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | 10 | class DbRepositoryImpl(private val cartDao: CartDao) : DbRepository { 11 | override suspend fun saveItemToCart(product: AppProduct, quantity: Int) = withContext(Dispatchers.IO) { 12 | val cartProductId = cartDao.addItem( 13 | CartProductEntity( 14 | 0, 15 | product.id, 16 | product.name, 17 | product.price, 18 | product.calculateTotal(quantity), 19 | quantity 20 | ) 21 | ) 22 | 23 | product.choices?.forEach { choice -> 24 | choice.options.filter { it.isChecked }.forEach { option -> 25 | val optionEntity = CartOptionEntity( 26 | 0, 27 | choice.id.toLong(), 28 | choice.name, 29 | option.id.toLong(), 30 | option.name, 31 | option.price, 32 | cartProductId 33 | ) 34 | cartDao.addItem(optionEntity) 35 | } 36 | } 37 | 38 | Unit 39 | } 40 | 41 | override suspend fun getCartItems(): List = withContext(Dispatchers.IO) { 42 | cartDao.getAllWithOptions() 43 | } 44 | 45 | override suspend fun deleteItemFromCart(item: CartItem) = withContext(Dispatchers.IO) { 46 | cartDao.deleteItem(item.cartItem) 47 | } 48 | 49 | override suspend fun clearCart() = withContext(Dispatchers.IO) { 50 | cartDao.deleteAll() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/test/java/com/marknkamau/justjava/utils/DateTimeTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | import org.junit.Assert 4 | import org.junit.Before 5 | import org.junit.Test 6 | import java.util.Date 7 | import java.util.TimeZone 8 | 9 | class DateTimeTest { 10 | 11 | @Before 12 | fun setup() { 13 | // Tests are configured based on GMT +3 14 | // Running on CI with a different timezone will result in errors 15 | TimeZone.setDefault(TimeZone.getTimeZone("Africa/Nairobi")) 16 | } 17 | 18 | @Test 19 | fun `converts from date`() { 20 | val dateTime = Date(1514784600L * 1000).toDateTime() 21 | 22 | Assert.assertEquals(2018, dateTime.year) 23 | Assert.assertEquals(1, dateTime.month) 24 | Assert.assertEquals(1, dateTime.dayOfMonth) 25 | Assert.assertEquals(8, dateTime.hourOfDay) 26 | Assert.assertEquals(30, dateTime.minute) 27 | Assert.assertEquals(0, dateTime.second) 28 | } 29 | 30 | @Test 31 | fun `converts to timestamp`() { 32 | val dateTime = DateTime(2018, 1, 1, 8, 30, 0) 33 | Assert.assertEquals(1514784600, dateTime.timestamp) 34 | } 35 | 36 | @Test 37 | fun `converts from timestamp`() { 38 | val dateTime = DateTime.fromTimestamp(1514784600) 39 | 40 | Assert.assertEquals(2018, dateTime.year) 41 | Assert.assertEquals(1, dateTime.month) 42 | Assert.assertEquals(1, dateTime.dayOfMonth) 43 | Assert.assertEquals(8, dateTime.hourOfDay) 44 | Assert.assertEquals(30, dateTime.minute) 45 | Assert.assertEquals(0, dateTime.second) 46 | } 47 | 48 | @Test 49 | fun `can format`() { 50 | val dateTime = DateTime(2001, 7, 4, 12, 8, 56) 51 | 52 | Assert.assertEquals("2001.07.04 AD at 12:08:56", dateTime.format("yyyy.MM.dd G 'at' HH:mm:ss")) 53 | } 54 | 55 | @Test 56 | fun `gets now`() { 57 | Assert.assertEquals(System.currentTimeMillis() / 1000, DateTime.now.timestamp) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/marknkamau/justjava/testUtils/TestRepositoriesModule.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.testUtils 2 | 3 | import com.marknjunge.core.data.local.PreferencesRepository 4 | import com.marknjunge.core.data.repository.* 5 | import com.marknkamau.justjava.data.db.DbRepository 6 | import com.marknkamau.justjava.di.RepositoriesModule 7 | import com.marknkamau.justjava.utils.NotificationHelper 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.components.SingletonComponent 11 | import dagger.hilt.testing.TestInstallIn 12 | import io.mockk.mockk 13 | 14 | @Module 15 | @TestInstallIn( 16 | components = [SingletonComponent::class], 17 | replaces = [RepositoriesModule::class] 18 | ) 19 | object TestRepositoriesModule { 20 | val mockPreferencesRepository = mockk() 21 | val mockNotificationHelper = mockk() 22 | 23 | val mockAuthRepository = mockk() 24 | val mockProductsRepository = mockk() 25 | val mockUsersRepository = mockk() 26 | val mockCartRepository = mockk() 27 | val mockOrdersRepository = mockk() 28 | val mockPaymentsRepository = mockk() 29 | 30 | val mockDbRepository = mockk() 31 | 32 | @Provides 33 | fun providesDbRepository(): DbRepository = mockDbRepository 34 | 35 | @Provides 36 | fun provideProductsRepository(): ProductsRepository = mockProductsRepository 37 | 38 | @Provides 39 | fun provideAuthRepository(): AuthRepository = mockAuthRepository 40 | 41 | @Provides 42 | fun provideCartRepository(): CartRepository = mockCartRepository 43 | 44 | @Provides 45 | fun provideOrdersRepository(): OrdersRepository = mockOrdersRepository 46 | 47 | @Provides 48 | fun providePaymentsRepository(): PaymentsRepository = mockPaymentsRepository 49 | 50 | @Provides 51 | fun provideUsersRepository(): UsersRepository = mockUsersRepository 52 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | ############################ 2 | # Retrofit 3 | ############################ 4 | # Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and 5 | # EnclosingMethod is required to use InnerClasses. 6 | -keepattributes Signature, InnerClasses, EnclosingMethod 7 | 8 | # Retain service method parameters when optimizing. 9 | -keepclassmembers,allowshrinking,allowobfuscation interface * { 10 | @retrofit2.http.* ; 11 | } 12 | 13 | # Ignore annotation used for build tooling. 14 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 15 | 16 | # Ignore JSR 305 annotations for embedding nullability information. 17 | -dontwarn javax.annotation.** 18 | 19 | # Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. 20 | -dontwarn kotlin.Unit 21 | 22 | # Top-level functions that can only be used by Kotlin. 23 | -dontwarn retrofit2.-KotlinExtensions 24 | 25 | ############################ 26 | # OkHttp 27 | ############################ 28 | # JSR 305 annotations are for embedding nullability information. 29 | -dontwarn javax.annotation.** 30 | 31 | # A resource is loaded with a relative path so the package of this class must be preserved. 32 | -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase 33 | 34 | # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. 35 | -dontwarn org.codehaus.mojo.animal_sniffer.* 36 | 37 | # OkHttp platform used only on JVM and when Conscrypt dependency is available. 38 | -dontwarn okhttp3.internal.platform.ConscryptPlatform 39 | 40 | ############################ 41 | # Okio 42 | ############################ 43 | # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. 44 | -dontwarn org.codehaus.mojo.animal_sniffer.* 45 | 46 | ############################ 47 | # Sentry 48 | ############################ 49 | -keepattributes LineNumberTable,SourceFile 50 | -dontwarn org.slf4j.** 51 | -dontwarn javax.** 52 | -keep class io.sentry.event.Event { *; } -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/di/RepositoriesModule.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.di 2 | 3 | import com.marknjunge.core.data.local.PreferencesRepository 4 | import com.marknjunge.core.data.network.service.* 5 | import com.marknjunge.core.data.repository.* 6 | import com.marknkamau.justjava.data.db.CartDao 7 | import com.marknkamau.justjava.data.db.DbRepository 8 | import com.marknkamau.justjava.data.db.DbRepositoryImpl 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.components.SingletonComponent 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | object RepositoriesModule { 17 | @Provides 18 | fun providesDbRepository(cardDao: CartDao): DbRepository = DbRepositoryImpl(cardDao) 19 | 20 | @Provides 21 | fun provideProductsRepository(apiService: ApiService): ProductsRepository = ApiProductsRepository(apiService) 22 | 23 | @Provides 24 | fun provideAuthRepository( 25 | authService: AuthService, 26 | preferencesRepository: PreferencesRepository, 27 | googleSignInService: GoogleSignInService 28 | ): AuthRepository = ApiAuthRepository(authService, preferencesRepository, googleSignInService) 29 | 30 | @Provides 31 | fun provideCartRepository(cartService: CartService): CartRepository = ApiCartRepository(cartService) 32 | 33 | @Provides 34 | fun provideOrdersRepository(ordersService: OrdersService): OrdersRepository = ApiOrdersRepository(ordersService) 35 | 36 | @Provides 37 | fun providePaymentsRepository( 38 | paymentsService: PaymentsService 39 | ): PaymentsRepository = ApiPaymentsRepository(paymentsService) 40 | 41 | @Provides 42 | fun provideUsersRepository( 43 | usersService: UsersService, 44 | preferencesRepository: PreferencesRepository, 45 | googleSignInService: GoogleSignInService, 46 | firebaseService: FirebaseService 47 | ): UsersRepository = ApiUsersRepository(usersService, preferencesRepository, googleSignInService, firebaseService) 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/addressBook/AddressBookViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.addressBook 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.marknjunge.core.data.model.Address 8 | import com.marknjunge.core.data.model.Resource 9 | import com.marknjunge.core.data.model.User 10 | import com.marknjunge.core.data.repository.UsersRepository 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.flow.collect 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class AddressBookViewModel @Inject constructor(private val usersRepository: UsersRepository) : ViewModel() { 18 | private val _loading = MutableLiveData() 19 | val loading: LiveData = _loading 20 | 21 | private val _user = MutableLiveData>() 22 | val user: LiveData> = _user 23 | 24 | fun getAddresses() { 25 | viewModelScope.launch { 26 | _loading.value = true 27 | usersRepository.getCurrentUser().collect { _user.value = it } 28 | _loading.value = false 29 | } 30 | } 31 | 32 | fun addAddress(address: Address): LiveData> { 33 | val liveData = MutableLiveData>() 34 | 35 | viewModelScope.launch { 36 | _loading.value = true 37 | liveData.value = usersRepository.addAddress(address) 38 | _loading.value = false 39 | } 40 | 41 | return liveData 42 | } 43 | 44 | fun deleteAddress(address: Address): LiveData> { 45 | val livedata = MutableLiveData>() 46 | 47 | viewModelScope.launch { 48 | _loading.value = true 49 | livedata.value = usersRepository.deleteAddress(address) 50 | _loading.value = false 51 | } 52 | 53 | return livedata 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/test/java/com/marknkamau/justjava/ui/productDetails/ProductDetailsViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.productDetails 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Observer 5 | import com.marknkamau.justjava.data.db.DbRepository 6 | import com.marknkamau.justjava.data.models.AppProduct 7 | import io.mockk.* 8 | import io.mockk.impl.annotations.MockK 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.ExperimentalCoroutinesApi 11 | import kotlinx.coroutines.test.TestCoroutineDispatcher 12 | import kotlinx.coroutines.test.resetMain 13 | import kotlinx.coroutines.test.setMain 14 | import org.junit.After 15 | import org.junit.Before 16 | import org.junit.Rule 17 | import org.junit.Test 18 | import org.junit.rules.TestRule 19 | 20 | class ProductDetailsViewModelTest { 21 | @get:Rule 22 | var rule: TestRule = InstantTaskExecutorRule() 23 | 24 | @ExperimentalCoroutinesApi 25 | private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() 26 | 27 | @MockK 28 | private lateinit var dbRepository: DbRepository 29 | 30 | private lateinit var viewModel: ProductDetailsViewModel 31 | 32 | @ExperimentalCoroutinesApi 33 | @Before 34 | fun setup() { 35 | MockKAnnotations.init(this, relaxUnitFun = true) 36 | viewModel = ProductDetailsViewModel(dbRepository) 37 | Dispatchers.setMain(testDispatcher) 38 | } 39 | 40 | @ExperimentalCoroutinesApi 41 | @After 42 | fun teardown() { 43 | Dispatchers.resetMain() 44 | testDispatcher.cleanupTestCoroutines() 45 | } 46 | 47 | @Test 48 | fun `can add item to cart`() { 49 | coEvery { dbRepository.saveItemToCart(any(), any()) } just runs 50 | 51 | val observer = spyk>() 52 | viewModel.addItemToCart(AppProduct(0L, "", "", "", 0L, 0.0, "", "", null, ""), 1).observeForever(observer) 53 | 54 | verify { observer.onChanged(Unit) } 55 | coVerify(exactly = 1) { dbRepository.saveItemToCart(any(), any()) } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/productDetails/OptionsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.productDetails 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.marknkamau.justjava.R 8 | import com.marknkamau.justjava.data.models.AppProductChoiceOption 9 | import com.marknkamau.justjava.databinding.ItemProductChoiceOptionBinding 10 | import com.marknkamau.justjava.utils.CurrencyFormatter 11 | 12 | class OptionsAdapter(private val context: Context) : RecyclerView.Adapter() { 13 | 14 | private val items by lazy { mutableListOf() } 15 | var onSelected: ((option: AppProductChoiceOption, checked: Boolean) -> Unit)? = null 16 | 17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 18 | val binding = ItemProductChoiceOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) 19 | return ViewHolder(binding) 20 | } 21 | 22 | override fun getItemCount() = items.size 23 | 24 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 25 | holder.bind(items[position], context) 26 | } 27 | 28 | fun setItems(newItems: List) { 29 | items.clear() 30 | items.addAll(newItems) 31 | notifyDataSetChanged() 32 | } 33 | 34 | inner class ViewHolder( 35 | private val binding: ItemProductChoiceOptionBinding 36 | ) : RecyclerView.ViewHolder(binding.root) { 37 | fun bind(option: AppProductChoiceOption, context: Context) { 38 | val formattedPrice = CurrencyFormatter.format(option.price) 39 | binding.tvOptionPrice.text = context.getString(R.string.price_listing_w_add, formattedPrice) 40 | binding.cbOptionTitle.text = option.name 41 | binding.cbOptionTitle.isChecked = option.isChecked 42 | 43 | binding.cbOptionTitle.setOnClickListener { 44 | onSelected?.invoke(option, binding.cbOptionTitle.isChecked) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | ############################ 2 | # JustJava 3 | ############################ 4 | -keep class com.marknjunge.core.data.model.** { *; } 5 | 6 | ############################ 7 | # Retrofit 8 | ############################ 9 | # Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and 10 | # EnclosingMethod is required to use InnerClasses. 11 | -keepattributes Signature, InnerClasses, EnclosingMethod 12 | 13 | # Retain service method parameters when optimizing. 14 | -keepclassmembers,allowshrinking,allowobfuscation interface * { 15 | @retrofit2.http.* ; 16 | } 17 | 18 | # Ignore annotation used for build tooling. 19 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 20 | 21 | # Ignore JSR 305 annotations for embedding nullability information. 22 | -dontwarn javax.annotation.** 23 | 24 | # Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. 25 | -dontwarn kotlin.Unit 26 | 27 | # Top-level functions that can only be used by Kotlin. 28 | -dontwarn retrofit2.-KotlinExtensions 29 | 30 | ############################ 31 | # OkHttp 32 | ############################ 33 | # JSR 305 annotations are for embedding nullability information. 34 | -dontwarn javax.annotation.** 35 | 36 | # A resource is loaded with a relative path so the package of this class must be preserved. 37 | -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase 38 | 39 | # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. 40 | -dontwarn org.codehaus.mojo.animal_sniffer.* 41 | 42 | # OkHttp platform used only on JVM and when Conscrypt dependency is available. 43 | -dontwarn okhttp3.internal.platform.ConscryptPlatform 44 | 45 | ############################ 46 | # Okio 47 | ############################ 48 | # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. 49 | -dontwarn org.codehaus.mojo.animal_sniffer.* 50 | 51 | -keep class com.google.android.gms.** { *; } 52 | -dontwarn com.google.android.gms.** 53 | 54 | ############################ 55 | # Coroutines 56 | ############################ 57 | -dontwarn kotlinx.coroutines.** -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'kotlin-parcelize' 5 | apply plugin: 'kotlinx-serialization' 6 | 7 | def debugApiBaseUrl = "\"https://justjava.marknjunge.com/\"" 8 | def localPropertiesFile = rootProject.file("local.properties") 9 | if (localPropertiesFile.exists()) { 10 | def localProperties = new Properties() 11 | localProperties.load(new FileInputStream(localPropertiesFile)) 12 | 13 | // Ability to provide a different debug url 14 | def overrideUrl = localProperties.getProperty("debugApiBaseUrl") 15 | if (overrideUrl != null) { 16 | debugApiBaseUrl = overrideUrl 17 | } 18 | } 19 | 20 | android { 21 | 22 | buildTypes { 23 | debug { 24 | minifyEnabled false 25 | buildConfigField "String", "API_BASE_URL", debugApiBaseUrl 26 | } 27 | release { 28 | minifyEnabled true 29 | consumerProguardFiles 'proguard-rules.pro' 30 | buildConfigField "String", "API_BASE_URL", "\"https://justjava.marknjunge.com/\"" 31 | } 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation fileTree(dir: 'libs', include: ['*.jar']) 37 | 38 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0" 39 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1" 40 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0" 41 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.5.0" 42 | 43 | implementation "androidx.appcompat:appcompat:1.3.0" 44 | 45 | // REST 46 | implementation "com.squareup.retrofit2:retrofit:2.9.0" 47 | implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" 48 | implementation "com.squareup.okhttp3:logging-interceptor:4.9.0" 49 | 50 | // Other 51 | implementation "com.jakewharton.timber:timber:4.7.1" 52 | 53 | testImplementation "junit:junit:4.13" 54 | androidTestImplementation "androidx.test:runner:1.3.0-rc01" 55 | androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0-rc01" 56 | testImplementation "io.mockk:mockk:1.11.0" 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/ToolbarActivity.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui 2 | 3 | import android.content.Intent 4 | import android.view.Menu 5 | import android.view.MenuItem 6 | import com.marknjunge.core.data.local.PreferencesRepository 7 | import com.marknkamau.justjava.R 8 | import com.marknkamau.justjava.ui.about.AboutActivity 9 | import com.marknkamau.justjava.ui.base.BaseActivity 10 | import com.marknkamau.justjava.ui.cart.CartActivity 11 | import com.marknkamau.justjava.ui.login.SignInActivity 12 | import com.marknkamau.justjava.ui.profile.ProfileActivity 13 | import javax.inject.Inject 14 | 15 | abstract class ToolbarActivity : BaseActivity() { 16 | 17 | @Inject lateinit var preferencesRepository: PreferencesRepository 18 | 19 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 20 | val inflater = menuInflater 21 | inflater.inflate(R.menu.overflow_menu, menu) 22 | return true 23 | } 24 | 25 | override fun onPrepareOptionsMenu(menu: Menu?): Boolean { 26 | super.onPrepareOptionsMenu(menu) 27 | if (this is CartActivity) { 28 | menu?.findItem(R.id.menu_cart)?.isVisible = false 29 | } 30 | if (this is ProfileActivity) { 31 | menu?.findItem(R.id.menu_profile)?.isVisible = false 32 | } 33 | 34 | return true 35 | } 36 | 37 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 38 | return when (item.itemId) { 39 | R.id.menu_cart -> { 40 | startActivity(Intent(this, CartActivity::class.java)) 41 | true 42 | } 43 | R.id.menu_profile -> { 44 | if (preferencesRepository.isSignedIn) { 45 | startActivity(Intent(this, ProfileActivity::class.java)) 46 | } else { 47 | startActivity(Intent(this, SignInActivity::class.java)) 48 | } 49 | true 50 | } 51 | R.id.menu_about -> { 52 | startActivity(Intent(this, AboutActivity::class.java)) 53 | true 54 | } 55 | else -> super.onOptionsItemSelected(item) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/test/java/com/marknkamau/justjava/ui/main/MainViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.main 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Observer 5 | import com.marknjunge.core.data.model.Product 6 | import com.marknjunge.core.data.model.Resource 7 | import com.marknjunge.core.data.repository.ProductsRepository 8 | import io.mockk.MockKAnnotations 9 | import io.mockk.coEvery 10 | import io.mockk.impl.annotations.MockK 11 | import io.mockk.spyk 12 | import io.mockk.verify 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.ExperimentalCoroutinesApi 15 | import kotlinx.coroutines.test.TestCoroutineDispatcher 16 | import kotlinx.coroutines.test.resetMain 17 | import kotlinx.coroutines.test.setMain 18 | import org.junit.After 19 | import org.junit.Before 20 | import org.junit.Rule 21 | import org.junit.Test 22 | import org.junit.rules.TestRule 23 | 24 | class MainViewModelTest { 25 | @get:Rule 26 | var rule: TestRule = InstantTaskExecutorRule() 27 | 28 | @ExperimentalCoroutinesApi 29 | private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() 30 | 31 | @MockK 32 | private lateinit var productsRepository: ProductsRepository 33 | 34 | private lateinit var viewModel: MainViewModel 35 | 36 | @ExperimentalCoroutinesApi 37 | @Before 38 | fun setup() { 39 | MockKAnnotations.init(this, relaxUnitFun = true) 40 | viewModel = MainViewModel(productsRepository) 41 | Dispatchers.setMain(testDispatcher) 42 | } 43 | 44 | @ExperimentalCoroutinesApi 45 | @After 46 | fun teardown() { 47 | Dispatchers.resetMain() 48 | testDispatcher.cleanupTestCoroutines() 49 | } 50 | 51 | @Test 52 | fun `can load products`() { 53 | val products = listOf() 54 | val resource = Resource.Success(products) 55 | coEvery { productsRepository.getProducts() } returns resource 56 | 57 | val observer = spyk>>>() 58 | viewModel.products.observeForever(observer) 59 | viewModel.getProducts() 60 | 61 | verify { observer.onChanged(resource) } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/test/java/com/marknkamau/justjava/ui/orders/OrdersViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.orders 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Observer 5 | import com.marknjunge.core.data.model.Order 6 | import com.marknjunge.core.data.model.Resource 7 | import com.marknjunge.core.data.repository.OrdersRepository 8 | import com.marknkamau.justjava.utils.SampleData 9 | import io.mockk.MockKAnnotations 10 | import io.mockk.coEvery 11 | import io.mockk.impl.annotations.MockK 12 | import io.mockk.spyk 13 | import io.mockk.verify 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.ExperimentalCoroutinesApi 16 | import kotlinx.coroutines.test.TestCoroutineDispatcher 17 | import kotlinx.coroutines.test.resetMain 18 | import kotlinx.coroutines.test.setMain 19 | import org.junit.After 20 | import org.junit.Before 21 | import org.junit.Rule 22 | import org.junit.Test 23 | import org.junit.rules.TestRule 24 | 25 | class OrdersViewModelTest { 26 | 27 | @get:Rule 28 | var rule: TestRule = InstantTaskExecutorRule() 29 | 30 | @ExperimentalCoroutinesApi 31 | private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() 32 | 33 | @MockK 34 | private lateinit var ordersRepository: OrdersRepository 35 | 36 | private lateinit var viewModel: OrdersViewModel 37 | 38 | @ExperimentalCoroutinesApi 39 | @Before 40 | fun setup() { 41 | MockKAnnotations.init(this, relaxUnitFun = true) 42 | Dispatchers.setMain(testDispatcher) 43 | viewModel = OrdersViewModel(ordersRepository) 44 | } 45 | 46 | @ExperimentalCoroutinesApi 47 | @After 48 | fun teardown() { 49 | Dispatchers.resetMain() 50 | testDispatcher.cleanupTestCoroutines() 51 | } 52 | 53 | @Test 54 | fun `can get orders`() { 55 | val resource = Resource.Success(listOf(SampleData.order)) 56 | coEvery { ordersRepository.getOrders() } returns resource 57 | 58 | val observer = spyk>>>() 59 | viewModel.orders.observeForever(observer) 60 | viewModel.getOrders() 61 | 62 | verify { observer.onChanged(resource) } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/about/AboutActivity.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.about 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.view.View 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.marknkamau.justjava.BuildConfig 10 | import com.marknkamau.justjava.databinding.ActivityAboutBinding 11 | 12 | class AboutActivity : AppCompatActivity() { 13 | 14 | private lateinit var binding: ActivityAboutBinding 15 | 16 | @SuppressLint("SetTextI18n") 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | binding = ActivityAboutBinding.inflate(layoutInflater) 20 | setContentView(binding.root) 21 | 22 | binding.imgBackAbout.setOnClickListener { finish() } 23 | val appVersion = "v${BuildConfig.VERSION_NAME} ${if (BuildConfig.BUILD_TYPE == "debug") "(debug)" else ""}" 24 | binding.tvAppVersionAbout.text = appVersion 25 | binding.tvSourceCodeAbout.setOnClickListener { openUrl("https://github.com/MarkNjunge/JustJava-Android") } 26 | binding.imgEmailAbout.setOnClickListener { sendEmail() } 27 | binding.imgLinkedInAbout.setOnClickListener { openUrl("https://linkedin.com/in/marknjunge") } 28 | binding.imgWebsiteAbout.setOnClickListener { openUrl("https://marknjunge.com") } 29 | binding.imgGithubAbout.setOnClickListener { openUrl("https://github.com/MarkNjunge") } 30 | binding.tvPrivacyPolicyAbout.setOnClickListener { openUrl("https://justjava.store/privacy") } 31 | 32 | // See https://github.com/google/play-services-plugins/pull/62 33 | binding.tvLicensesAbout.visibility = View.GONE 34 | } 35 | 36 | private fun openUrl(url: String) = startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) 37 | 38 | private fun sendEmail() { 39 | val addresses = arrayOf("contact@marknjunge.com") // Has to be String array or it will ignore 40 | val intent = Intent(Intent.ACTION_SENDTO) 41 | intent.data = Uri.parse("mailto:") // only email apps should handle this 42 | intent.putExtra(Intent.EXTRA_EMAIL, addresses) 43 | startActivity(intent) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/test/java/com/marknkamau/justjava/ui/payCard/PayCardViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.payCard 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Observer 5 | import com.marknjunge.core.data.model.ApiResponse 6 | import com.marknjunge.core.data.model.Resource 7 | import com.marknjunge.core.data.repository.PaymentsRepository 8 | import io.mockk.MockKAnnotations 9 | import io.mockk.coEvery 10 | import io.mockk.impl.annotations.MockK 11 | import io.mockk.spyk 12 | import io.mockk.verify 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.ExperimentalCoroutinesApi 15 | import kotlinx.coroutines.test.TestCoroutineDispatcher 16 | import kotlinx.coroutines.test.resetMain 17 | import kotlinx.coroutines.test.setMain 18 | import org.junit.After 19 | import org.junit.Before 20 | import org.junit.Rule 21 | import org.junit.Test 22 | import org.junit.rules.TestRule 23 | 24 | class PayCardViewModelTest { 25 | @get:Rule 26 | var rule: TestRule = InstantTaskExecutorRule() 27 | 28 | @ExperimentalCoroutinesApi 29 | private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() 30 | 31 | @MockK 32 | private lateinit var paymentsRepository: PaymentsRepository 33 | 34 | private lateinit var viewModel: PayCardViewModel 35 | 36 | @ExperimentalCoroutinesApi 37 | @Before 38 | fun setup() { 39 | MockKAnnotations.init(this, relaxUnitFun = true) 40 | Dispatchers.setMain(testDispatcher) 41 | viewModel = PayCardViewModel(paymentsRepository) 42 | } 43 | 44 | @ExperimentalCoroutinesApi 45 | @After 46 | fun teardown() { 47 | Dispatchers.resetMain() 48 | testDispatcher.cleanupTestCoroutines() 49 | } 50 | 51 | @Test 52 | fun `can initiate card payment`() { 53 | val resource = Resource.Success(ApiResponse("")) 54 | coEvery { 55 | paymentsRepository.initiateCardPayment( 56 | any(), 57 | any() 58 | ) 59 | } returns resource 60 | 61 | val observer = spyk>>() 62 | viewModel.initiateCardPayment("", "", "", "", "").observeForever(observer) 63 | 64 | verify { observer.onChanged(resource) } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/cart/CartViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.cart 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.marknjunge.core.data.local.PreferencesRepository 8 | import com.marknjunge.core.data.model.* 9 | import com.marknjunge.core.data.repository.CartRepository 10 | import com.marknkamau.justjava.data.db.DbRepository 11 | import com.marknkamau.justjava.data.models.CartItem 12 | import dagger.hilt.android.lifecycle.HiltViewModel 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class CartViewModel @Inject constructor( 18 | private val preferencesRepository: PreferencesRepository, 19 | private val dbRepository: DbRepository, 20 | private val cartRepository: CartRepository 21 | ) : 22 | ViewModel() { 23 | 24 | private val _items = MutableLiveData>() 25 | val items: LiveData> = _items 26 | 27 | private val _loading = MutableLiveData() 28 | val loading: LiveData = _loading 29 | 30 | fun isSignedIn() = preferencesRepository.isSignedIn 31 | 32 | fun getCartItems() { 33 | viewModelScope.launch { 34 | _items.value = dbRepository.getCartItems() 35 | } 36 | } 37 | 38 | fun deleteItem(item: CartItem) { 39 | viewModelScope.launch { 40 | dbRepository.deleteItemFromCart(item) 41 | _items.value = dbRepository.getCartItems() 42 | } 43 | } 44 | 45 | fun verifyOrder(items: List): LiveData>> { 46 | val livedata = MutableLiveData>>() 47 | 48 | viewModelScope.launch { 49 | _loading.value = true 50 | val verificationDto = items.mapIndexed { index, item -> 51 | val options = 52 | item.options.map { VerifyOrderItemOptionDto(it.choiceId, it.optionId, it.optionPrice) } 53 | VerifyOrderItemDto(index, item.cartItem.productId, item.cartItem.productBasePrice, options) 54 | } 55 | 56 | livedata.value = cartRepository.verifyOrder(VerifyOrderDto(verificationDto)) 57 | _loading.value = false 58 | } 59 | 60 | return livedata 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/login/SignInViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.login 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.marknjunge.core.data.model.ApiResponse 8 | import com.marknjunge.core.data.model.Resource 9 | import com.marknjunge.core.data.model.User 10 | import com.marknjunge.core.data.repository.AuthRepository 11 | import com.marknjunge.core.data.repository.UsersRepository 12 | import dagger.hilt.android.lifecycle.HiltViewModel 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class SignInViewModel @Inject constructor( 18 | private val authRepository: AuthRepository, 19 | private val usersRepository: UsersRepository 20 | ) : ViewModel() { 21 | private val _loading = MutableLiveData() 22 | val loading: LiveData = _loading 23 | 24 | fun signInWithGoogle(idToken: String): LiveData> { 25 | val liveData = MutableLiveData>() 26 | 27 | viewModelScope.launch { 28 | _loading.value = true 29 | liveData.value = authRepository.signInWithGoogle(idToken) 30 | 31 | if (liveData.value is Resource.Success) { 32 | usersRepository.updateFcmToken() 33 | } 34 | 35 | _loading.value = false 36 | } 37 | 38 | return liveData 39 | } 40 | 41 | fun signIn(email: String, password: String): LiveData> { 42 | val liveData = MutableLiveData>() 43 | 44 | viewModelScope.launch { 45 | _loading.value = true 46 | liveData.value = authRepository.signIn(email, password) 47 | 48 | if (liveData.value is Resource.Success) { 49 | usersRepository.updateFcmToken() 50 | } 51 | 52 | _loading.value = false 53 | } 54 | 55 | return liveData 56 | } 57 | 58 | fun requestPasswordReset(email: String): LiveData> { 59 | val liveData = MutableLiveData>() 60 | 61 | viewModelScope.launch { 62 | _loading.value = true 63 | liveData.value = authRepository.requestPasswordReset(email) 64 | _loading.value = false 65 | } 66 | 67 | return liveData 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/test/java/com/marknkamau/justjava/ui/payMpesa/PayMpesaViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.payMpesa 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Observer 5 | import com.marknjunge.core.data.local.PreferencesRepository 6 | import com.marknjunge.core.data.model.ApiResponse 7 | import com.marknjunge.core.data.model.Resource 8 | import com.marknjunge.core.data.repository.PaymentsRepository 9 | import com.marknkamau.justjava.utils.SampleData 10 | import io.mockk.* 11 | import io.mockk.impl.annotations.MockK 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.ExperimentalCoroutinesApi 14 | import kotlinx.coroutines.test.TestCoroutineDispatcher 15 | import kotlinx.coroutines.test.resetMain 16 | import kotlinx.coroutines.test.setMain 17 | import org.junit.* 18 | import org.junit.rules.TestRule 19 | 20 | class PayMpesaViewModelTest { 21 | @get:Rule 22 | var rule: TestRule = InstantTaskExecutorRule() 23 | 24 | @ExperimentalCoroutinesApi 25 | private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() 26 | 27 | @MockK 28 | private lateinit var preferencesRepository: PreferencesRepository 29 | 30 | @MockK 31 | private lateinit var paymentsRepository: PaymentsRepository 32 | 33 | private lateinit var viewModel: PayMpesaViewModel 34 | 35 | @ExperimentalCoroutinesApi 36 | @Before 37 | fun setup() { 38 | MockKAnnotations.init(this, relaxUnitFun = true) 39 | Dispatchers.setMain(testDispatcher) 40 | viewModel = PayMpesaViewModel(preferencesRepository, paymentsRepository) 41 | } 42 | 43 | @ExperimentalCoroutinesApi 44 | @After 45 | fun teardown() { 46 | Dispatchers.resetMain() 47 | testDispatcher.cleanupTestCoroutines() 48 | } 49 | 50 | @Test 51 | fun `can get user`() { 52 | every { preferencesRepository.user } returns SampleData.user 53 | 54 | Assert.assertEquals(SampleData.user, viewModel.getUser()) 55 | } 56 | 57 | @Test 58 | fun `can initiate mpesa payment`() { 59 | val resource = Resource.Success(ApiResponse("")) 60 | coEvery { paymentsRepository.requestMpesa(any(), any()) } returns resource 61 | 62 | val observer = spyk>>() 63 | viewModel.payMpesa("", "").observeForever(observer) 64 | 65 | verify { observer.onChanged(resource) } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Changelog.MD: -------------------------------------------------------------------------------- 1 | # JustJava changelog 2 | 3 | ## 2.2.1 4 | 5 | - Handle rejected session-id 6 | 7 | ## 2.2.0 8 | - Bias places autocomplete to Nairobi 9 | - Remove mobile number requirement 10 | - Fix invalid session-id errors by logging out 11 | - Add option to change payment method 12 | - Disable place order button when there is no no delivery address 13 | - Fix payments notification channel being the same as orders channel 14 | - Add illustration when there aren't any orders 15 | - Add pull to refresh in products screen 16 | 17 | ## 2.1.0 18 | 19 | - Improve app launch time 20 | - Remove mobile number length restriction 21 | - Add delete account feature 22 | - Add password reset 23 | - Update login and signup design 24 | 25 | ## 2.0.0 26 | 27 | - Change backend from Firebase 28 | - Add Google Sign In 29 | 30 | - Use dynamic products list 31 | - Add Google Sign In 32 | - Add ability to have multiple delivery addresses 33 | - Add card payment 34 | - Disable backup on uninstall 35 | 36 | ## 1.7.0 37 | 38 | - Update UI design 39 | - Remove checkout screen and combine it with cart 40 | - Show only the last 3 orders in the profile screen. 41 | - Add new screen to show all past orders 42 | - Change item images 43 | 44 | ## 1.6.0 45 | 46 | - Fix input field colors 47 | - Save notification token to user's profile 48 | - Update notifications 49 | - Match edit cart item dialog to match rest of app 50 | - Numerous UI changes 51 | 52 | ## 1.5.0 53 | 54 | - Move open source libraries list to separate page. 55 | - Dialog to edit the cart shows up when an item is clicked. 56 | - Update profile screen design. 57 | - Update privacy policy url and contact email. 58 | 59 | ## 1.4.1 60 | 61 | - Minor UI change from switching to new material library. 62 | 63 | ## 1.4.0 64 | 65 | - Fix critical bug that prevented new accounts from being created. 66 | 67 | ## 1.3.0 68 | 69 | - Profile icon is always present. 70 | - Fix text wrapping on catalog. 71 | - Various changes to sign up. 72 | 73 | ## 1.2.1 74 | 75 | - Fix crash reporting 76 | 77 | ## 1.2.0 78 | 79 | - Enable editing customer details - [812538a](https://github.com/MarkNjunge/JustJava-Android/commit/812538a12dd6a16f7623ab9531135538f627a86b) 80 | - Fix privacy policy link - [e1d0d71](https://github.com/MarkNjunge/JustJava-Android/commit/e1d0d714cea9c928e3662d2f020a6413ff48cadf) 81 | 82 | ## 1.1.0 83 | 84 | - Fix M-Pesa payment - [a11f8a3](https://github.com/MarkNjunge/JustJava-Android/commit/a11f8a35136b1e1d2d038e7588d1070c2e4fac1c) 85 | -------------------------------------------------------------------------------- /app/src/test/java/com/marknkamau/justjava/ui/orderDetail/OrderDetailViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.orderDetail 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Observer 5 | import com.marknjunge.core.data.local.PreferencesRepository 6 | import com.marknjunge.core.data.model.Order 7 | import com.marknjunge.core.data.model.Resource 8 | import com.marknjunge.core.data.repository.OrdersRepository 9 | import com.marknkamau.justjava.utils.SampleData 10 | import io.mockk.* 11 | import io.mockk.impl.annotations.MockK 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.ExperimentalCoroutinesApi 14 | import kotlinx.coroutines.test.TestCoroutineDispatcher 15 | import kotlinx.coroutines.test.resetMain 16 | import kotlinx.coroutines.test.setMain 17 | import org.junit.* 18 | import org.junit.rules.TestRule 19 | 20 | class OrderDetailViewModelTest { 21 | 22 | @get:Rule 23 | var rule: TestRule = InstantTaskExecutorRule() 24 | 25 | @ExperimentalCoroutinesApi 26 | private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() 27 | 28 | @MockK 29 | private lateinit var ordersRepository: OrdersRepository 30 | 31 | @MockK 32 | private lateinit var preferencesRepository: PreferencesRepository 33 | 34 | private lateinit var viewModel: OrderDetailViewModel 35 | 36 | @ExperimentalCoroutinesApi 37 | @Before 38 | fun setup() { 39 | MockKAnnotations.init(this, relaxUnitFun = true) 40 | Dispatchers.setMain(testDispatcher) 41 | viewModel = OrderDetailViewModel(ordersRepository, preferencesRepository) 42 | } 43 | 44 | @ExperimentalCoroutinesApi 45 | @After 46 | fun teardown() { 47 | Dispatchers.resetMain() 48 | testDispatcher.cleanupTestCoroutines() 49 | } 50 | 51 | @Test 52 | fun `can get order`() { 53 | val resource = Resource.Success(SampleData.order) 54 | coEvery { ordersRepository.getOrderById(any()) } returns resource 55 | 56 | val observer = spyk>>() 57 | viewModel.order.observeForever(observer) 58 | viewModel.getOrder("") 59 | 60 | verify { observer.onChanged(resource) } 61 | } 62 | 63 | @Test 64 | fun `can get user`() { 65 | every { preferencesRepository.user } returns SampleData.user 66 | 67 | Assert.assertEquals(SampleData.user, viewModel.getUser()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/utils/DateTime.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | import android.annotation.SuppressLint 4 | import java.text.SimpleDateFormat 5 | import java.util.* 6 | 7 | /** 8 | * A class to be used to easily handle dates. 9 | * Months begin at 1 10 | */ 11 | data class DateTime( 12 | var year: Int, 13 | var month: Int, 14 | var dayOfMonth: Int, 15 | var hourOfDay: Int, 16 | var minute: Int, 17 | var second: Int = 0 18 | ) { 19 | 20 | companion object { 21 | /** 22 | * Returns the current time as a DateTime object. 23 | */ 24 | val now: DateTime 25 | get() = Date(System.currentTimeMillis()).toDateTime() 26 | 27 | /** 28 | * Converts a timestamp(in seconds) to a DateTime object 29 | */ 30 | fun fromTimestamp(timestamp: Long): DateTime = Date(timestamp * 1000).toDateTime() 31 | } 32 | 33 | /** 34 | * Time in seconds. 35 | */ 36 | val timestamp: Long 37 | get() { 38 | val now = Calendar.getInstance() 39 | now.set(this.year, this.month - 1, this.dayOfMonth, this.hourOfDay, this.minute, this.second) 40 | return now.time.time / 1000L 41 | } 42 | 43 | /** 44 | * Formats the dateTime as the given format 45 | */ 46 | @SuppressLint("SimpleDateFormat") 47 | fun format(format: String): String { 48 | val now = Calendar.getInstance() 49 | now.set(this.year, this.month - 1, this.dayOfMonth, this.hourOfDay, this.minute, this.second) 50 | return now.time.format(format) 51 | } 52 | 53 | @SuppressLint("SimpleDateFormat") 54 | fun parse(format: String, source: String): DateTime? = SimpleDateFormat(format).parse(source)?.toDateTime() 55 | } 56 | 57 | /** 58 | * Converts a java date to a DateTime object. 59 | */ 60 | fun Date.toDateTime(): DateTime { 61 | val hourOfDay = this.format("H").toInt() // Format according to 24Hr from 0-23 62 | val minute = this.format("m").toInt() 63 | val year = this.format("yyyy").toInt() 64 | val month = this.format("M").toInt() 65 | val dayOfMonth = this.format("dd").toInt() 66 | val second = this.format("s").toInt() 67 | 68 | return DateTime(year, month, dayOfMonth, hourOfDay, minute, second) 69 | } 70 | 71 | /** 72 | * Helper function to format dates. 73 | */ 74 | @SuppressLint("SimpleDateFormat") 75 | fun Date.format(pattern: String): String = SimpleDateFormat(pattern).format(this) 76 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/marknkamau/justjava/SmokeTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava 2 | 3 | import androidx.test.core.app.ActivityScenario 4 | import com.marknjunge.core.data.model.Resource 5 | import com.marknkamau.justjava.testUtils.* 6 | import com.marknkamau.justjava.ui.main.MainActivity 7 | import dagger.hilt.android.testing.HiltAndroidRule 8 | import dagger.hilt.android.testing.HiltAndroidTest 9 | import io.mockk.coEvery 10 | import io.mockk.just 11 | import io.mockk.runs 12 | import org.junit.Rule 13 | import org.junit.Test 14 | 15 | @HiltAndroidTest 16 | class SmokeTest { 17 | 18 | @get:Rule(order = 0) 19 | var hiltRule = HiltAndroidRule(this) 20 | 21 | @Test 22 | fun canPerformAppFunctions() { 23 | setupMockResponses() 24 | 25 | ActivityScenario.launch(MainActivity::class.java) 26 | 27 | // Go to productDetails 28 | onViewWithId(R.id.rvProducts).clickRecyclerViewItem(0) 29 | 30 | // Add item to cart 31 | onViewWithId(R.id.btnAddToCart).click() 32 | 33 | // Go to cart 34 | onViewWithId(R.id.menu_cart).click() 35 | 36 | // Go to checkout 37 | onViewWithId(R.id.btnCheckout).click() 38 | 39 | // Place order 40 | onViewWithId(R.id.btnPlaceOrder).click() 41 | } 42 | 43 | private fun setupMockResponses() { 44 | coEvery { TestRepositoriesModule.mockProductsRepository.getProducts() } returns Resource.Success( 45 | listOf( 46 | SampleData.product 47 | ) 48 | ) 49 | coEvery { TestRepositoriesModule.mockPreferencesRepository.isSignedIn } returns true 50 | coEvery { TestRepositoriesModule.mockPreferencesRepository.user } returns SampleData.user 51 | coEvery { TestRepositoriesModule.mockDbRepository.saveItemToCart(any(), any()) } just runs 52 | coEvery { TestRepositoriesModule.mockDbRepository.getCartItems() } returns SampleData.cartItems 53 | coEvery { TestRepositoriesModule.mockCartRepository.verifyOrder(any()) } returns Resource.Success( 54 | listOf( 55 | SampleData.verifyOrderResponse 56 | ) 57 | ) 58 | coEvery { TestRepositoriesModule.mockOrdersRepository.placeOrder(any()) } returns Resource.Success(SampleData.order) 59 | coEvery { TestRepositoriesModule.mockDbRepository.clearCart() } just runs 60 | coEvery { TestRepositoriesModule.mockOrdersRepository.getOrderById(any()) } returns Resource.Success(SampleData.order) 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/profile/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.profile 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.marknjunge.core.data.model.Resource 8 | import com.marknjunge.core.data.model.User 9 | import com.marknjunge.core.data.repository.AuthRepository 10 | import com.marknjunge.core.data.repository.UsersRepository 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.flow.collect 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class ProfileViewModel @Inject constructor( 18 | private val usersRepository: UsersRepository, 19 | private val authRepository: AuthRepository 20 | ) : ViewModel() { 21 | private val _loading = MutableLiveData() 22 | val loading: LiveData = _loading 23 | 24 | private val _user = MutableLiveData>() 25 | val user: LiveData> = _user 26 | 27 | fun getCurrentUser() { 28 | viewModelScope.launch { 29 | _loading.value = true 30 | usersRepository.getCurrentUser().collect { _user.value = it } 31 | _loading.value = false 32 | } 33 | } 34 | 35 | fun updateUser(firstName: String, lastName: String, mobile: String, email: String): LiveData> { 36 | val livedata = MutableLiveData>() 37 | 38 | viewModelScope.launch { 39 | _loading.value = true 40 | livedata.value = usersRepository.updateUser(firstName, lastName, mobile, email) 41 | _loading.value = false 42 | } 43 | 44 | return livedata 45 | } 46 | 47 | fun signOut(): LiveData> { 48 | val livedata = MutableLiveData>() 49 | 50 | viewModelScope.launch { 51 | _loading.value = true 52 | livedata.value = authRepository.signOut() 53 | _loading.value = false 54 | } 55 | 56 | return livedata 57 | } 58 | 59 | fun deleteAccount(): LiveData> { 60 | val livedata = MutableLiveData>() 61 | 62 | viewModelScope.launch { 63 | _loading.value = true 64 | livedata.value = usersRepository.deleteUser() 65 | _loading.value = false 66 | } 67 | 68 | return livedata 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/utils/PreferenceUtils.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.utils 2 | 3 | import android.content.SharedPreferences 4 | import kotlinx.serialization.DeserializationStrategy 5 | import kotlinx.serialization.SerializationException 6 | import kotlinx.serialization.SerializationStrategy 7 | import kotlinx.serialization.json.Json 8 | 9 | class PreferenceUtils(val preferences: SharedPreferences, val json: Json) { 10 | fun set(key: String, value: Any) { 11 | when (value) { 12 | is String -> edit { putString(key, value) } 13 | is Int -> edit { putInt(key, value) } 14 | is Boolean -> edit { putBoolean(key, value) } 15 | is Float -> edit { putFloat(key, value) } 16 | is Long -> edit { putLong(key, value) } 17 | else -> throw UnsupportedOperationException("Saving of ${value.javaClass.name} not yet implemented") 18 | } 19 | } 20 | 21 | inline fun get(key: String, defaultValue: T? = null): T { 22 | return preferences.run { 23 | when (T::class) { 24 | String::class -> getString(key, defaultValue as? String) as T 25 | Int::class -> getInt(key, defaultValue as? Int ?: -1) as T 26 | Boolean::class -> getBoolean(key, defaultValue as? Boolean ?: false) as T 27 | Float::class -> getFloat(key, defaultValue as? Float ?: -1f) as T 28 | Long::class -> getLong(key, defaultValue as? Long ?: -1) as T 29 | else -> throw UnsupportedOperationException("Retrieving of ${T::class.java.name} not yet implemented") 30 | } 31 | } 32 | } 33 | 34 | inline fun getObject(key: String, serializer: DeserializationStrategy): T? { 35 | return try { 36 | val s: String = get(key, "") 37 | json.decodeFromString(serializer, s) 38 | } catch (e: SerializationException) { 39 | // If it failed to parse the json, it means the value is null 40 | null 41 | } 42 | } 43 | 44 | inline fun setObject(key: String, value: T?, serializer: SerializationStrategy) { 45 | if (value == null) { 46 | set(key, "") 47 | } else { 48 | val s = json.encodeToString(serializer, value) 49 | set(key, s) 50 | } 51 | } 52 | 53 | private fun edit(block: SharedPreferences.Editor.() -> Unit) { 54 | preferences.edit().apply(block).apply() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_cart_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 30 | 31 | 44 | 45 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_product.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 28 | 29 | 30 | 31 | 43 | 44 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/test/java/com/marknkamau/justjava/ui/signup/SignUpViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.signup 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Observer 5 | import com.marknjunge.core.data.model.Resource 6 | import com.marknjunge.core.data.model.User 7 | import com.marknjunge.core.data.network.service.FirebaseService 8 | import com.marknjunge.core.data.repository.AuthRepository 9 | import com.marknjunge.core.data.repository.UsersRepository 10 | import io.mockk.MockKAnnotations 11 | import io.mockk.coEvery 12 | import io.mockk.impl.annotations.MockK 13 | import io.mockk.spyk 14 | import io.mockk.verify 15 | import kotlinx.coroutines.Dispatchers 16 | import kotlinx.coroutines.ExperimentalCoroutinesApi 17 | import kotlinx.coroutines.test.TestCoroutineDispatcher 18 | import kotlinx.coroutines.test.resetMain 19 | import kotlinx.coroutines.test.setMain 20 | import org.junit.After 21 | import org.junit.Before 22 | import org.junit.Rule 23 | import org.junit.Test 24 | import org.junit.rules.TestRule 25 | 26 | class SignUpViewModelTest { 27 | @get:Rule 28 | var rule: TestRule = InstantTaskExecutorRule() 29 | 30 | @ExperimentalCoroutinesApi 31 | private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() 32 | 33 | @MockK 34 | private lateinit var authRepository: AuthRepository 35 | 36 | @MockK 37 | private lateinit var usersRepository: UsersRepository 38 | 39 | @MockK 40 | private lateinit var firebaseService: FirebaseService 41 | 42 | private lateinit var viewModel: SignUpViewModel 43 | 44 | @ExperimentalCoroutinesApi 45 | @Before 46 | fun setup() { 47 | MockKAnnotations.init(this, relaxUnitFun = true) 48 | viewModel = SignUpViewModel(authRepository, usersRepository) 49 | Dispatchers.setMain(testDispatcher) 50 | 51 | coEvery { firebaseService.getFcmToken() } returns "" 52 | coEvery { usersRepository.updateFcmToken() } returns Resource.Success(Unit) 53 | } 54 | 55 | @ExperimentalCoroutinesApi 56 | @After 57 | fun teardown() { 58 | Dispatchers.resetMain() 59 | testDispatcher.cleanupTestCoroutines() 60 | } 61 | 62 | @Test 63 | fun `can sign up`() { 64 | val resource = Resource.Success(User(1, "", "", 0, "", "", "", "", listOf())) 65 | coEvery { authRepository.signUp(any(), any(), any(), any()) } returns resource 66 | 67 | val observer = spyk>>() 68 | viewModel.signUp("", "", "", "").observeForever(observer) 69 | 70 | verify { observer.onChanged(resource) } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_just_java_logo_black.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/JustJavaApp.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava 2 | 3 | import android.app.Application 4 | import com.google.android.libraries.places.api.Places 5 | import com.marknjunge.core.data.local.PreferencesRepository 6 | import com.marknjunge.core.data.model.Resource 7 | import com.marknjunge.core.data.repository.AuthRepository 8 | import com.marknjunge.core.data.repository.UsersRepository 9 | import com.marknkamau.justjava.utils.ReleaseTree 10 | import com.marknkamau.justjava.utils.toast 11 | import dagger.hilt.android.HiltAndroidApp 12 | import io.sentry.android.core.SentryAndroid 13 | import kotlinx.coroutines.CoroutineScope 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.flow.collect 16 | import kotlinx.coroutines.launch 17 | import timber.log.Timber 18 | import javax.inject.Inject 19 | 20 | @Suppress("unused") 21 | @HiltAndroidApp 22 | open class JustJavaApp : Application() { 23 | @Inject 24 | lateinit var preferencesRepository: PreferencesRepository 25 | @Inject 26 | lateinit var usersRepository: UsersRepository 27 | @Inject 28 | lateinit var authRepository: AuthRepository 29 | 30 | private val coroutineScope = CoroutineScope(Dispatchers.Main) 31 | 32 | override fun onCreate() { 33 | super.onCreate() 34 | 35 | if (BuildConfig.DEBUG) { 36 | Timber.plant(object : Timber.DebugTree() { 37 | override fun createStackElementTag(element: StackTraceElement): String { 38 | return "Timber ${element.methodName} (${element.fileName}:${element.lineNumber})" 39 | } 40 | }) 41 | } else { 42 | Timber.plant(ReleaseTree()) 43 | SentryAndroid.init(this) { options -> 44 | options.dsn = BuildConfig.sentryDsn 45 | } 46 | } 47 | 48 | Places.initialize(this, getString(R.string.google_api_key)) 49 | 50 | if (preferencesRepository.isSignedIn) { 51 | coroutineScope.launch { 52 | when (val resource = usersRepository.updateFcmToken()) { 53 | is Resource.Success -> usersRepository.getCurrentUser().collect { } 54 | is Resource.Failure -> { 55 | if (resource.response.message == "Invalid session-id") { 56 | Timber.d("Signed out") 57 | authRepository.signOutLocally() 58 | } else { 59 | toast(resource.response.message) 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.di 2 | 3 | import android.content.Context 4 | import com.google.android.gms.auth.api.signin.GoogleSignIn 5 | import com.google.android.gms.auth.api.signin.GoogleSignInClient 6 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions 7 | import com.marknjunge.core.data.local.PreferencesRepository 8 | import com.marknjunge.core.data.network.NetworkProvider 9 | import com.marknjunge.core.data.network.service.FirebaseService 10 | import com.marknjunge.core.data.network.service.GoogleSignInService 11 | import com.marknkamau.justjava.BuildConfig 12 | import com.marknkamau.justjava.data.network.AppFirebaseService 13 | import com.marknkamau.justjava.data.network.GoogleSignInServiceImpl 14 | import dagger.Module 15 | import dagger.Provides 16 | import dagger.hilt.InstallIn 17 | import dagger.hilt.android.qualifiers.ApplicationContext 18 | import dagger.hilt.components.SingletonComponent 19 | 20 | @Module 21 | @InstallIn(SingletonComponent::class) 22 | object NetworkModule { 23 | 24 | @Provides 25 | fun provideApiService(networkProvider: NetworkProvider) = networkProvider.apiService 26 | 27 | @Provides 28 | fun provideAuthService(networkProvider: NetworkProvider) = networkProvider.authService 29 | 30 | @Provides 31 | fun provideCartService(networkProvider: NetworkProvider) = networkProvider.cartService 32 | 33 | @Provides 34 | fun provideOrdersService(networkProvider: NetworkProvider) = networkProvider.ordersService 35 | 36 | @Provides 37 | fun providePaymentService(networkProvider: NetworkProvider) = networkProvider.paymentsService 38 | 39 | @Provides 40 | fun provideUsersService(networkProvider: NetworkProvider) = networkProvider.usersService 41 | 42 | @Provides 43 | fun provideFirebaseService(): FirebaseService = AppFirebaseService() 44 | 45 | @Provides 46 | fun provideGoogleSignInClient(@ApplicationContext context: Context): GoogleSignInClient { 47 | val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) 48 | .requestEmail() 49 | .requestIdToken(BuildConfig.googleClientId) 50 | .build() 51 | return GoogleSignIn.getClient(context, gso) 52 | } 53 | 54 | @Provides 55 | fun provideGoogleSignInService(client: GoogleSignInClient): GoogleSignInService = GoogleSignInServiceImpl(client) 56 | 57 | @Provides 58 | fun provideNetworkProvider( 59 | @ApplicationContext context: Context, 60 | preferencesRepository: PreferencesRepository 61 | ): NetworkProvider { 62 | return NetworkProvider(context, preferencesRepository) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_order_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 34 | 35 | 47 | 48 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_just_java_logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/data/models/AppProduct.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.data.models 2 | 3 | import com.marknjunge.core.data.model.Product 4 | import com.marknjunge.core.data.model.ProductChoice 5 | import com.marknjunge.core.data.model.ProductChoiceOption 6 | 7 | data class AppProduct( 8 | val id: Long, 9 | val name: String, 10 | val slug: String, 11 | val image: String, 12 | val createdAt: Long, 13 | val price: Double, 14 | val description: String, 15 | val type: String, 16 | var choices: List?, 17 | val status: String 18 | ) { 19 | fun calculateTotal(quantity: Int): Double { 20 | val basePrice = price 21 | 22 | val optionsTotal = choices?.fold(0.0) { i, c -> 23 | i + c.options 24 | .filter { it.isChecked } 25 | .fold(0.0) { acc, o -> acc + o.price } 26 | } ?: 0.0 27 | 28 | return (basePrice + optionsTotal) * quantity 29 | } 30 | 31 | fun validate(): MutableList { 32 | val errors = mutableListOf() 33 | choices?.forEach { choice -> 34 | if (choice.isRequired && !choice.hasValue) { 35 | errors.add(choice.name) 36 | } 37 | } 38 | 39 | return errors 40 | } 41 | } 42 | 43 | data class AppProductChoice( 44 | val id: Int, 45 | val name: String, 46 | val position: Int, 47 | val qtyMax: Int, 48 | val qtyMin: Int, 49 | var options: List 50 | ) { 51 | val isRequired: Boolean = qtyMin == 1 52 | val isSingleSelectable = qtyMax == 1 53 | val hasValue: Boolean 54 | get() = options.any { it.isChecked } 55 | } 56 | 57 | data class AppProductChoiceOption( 58 | val id: Int, 59 | val price: Double, 60 | val name: String, 61 | val description: String?, 62 | var isChecked: Boolean = false 63 | ) 64 | 65 | fun Product.toAppModel() = 66 | AppProduct( 67 | id, 68 | name, 69 | slug, 70 | image, 71 | createdAt, 72 | price, 73 | description, 74 | type, 75 | choices?.toAppChoice(), 76 | status 77 | ) 78 | 79 | private fun List.toAppChoice(): List = this.map { 80 | AppProductChoice( 81 | it.id, 82 | it.name, 83 | it.position, 84 | it.qtyMax, 85 | it.qtyMin, 86 | it.options.toAppOption() 87 | ) 88 | }.sortedBy { it.id } 89 | 90 | private fun List.toAppOption(): List = map { 91 | AppProductChoiceOption( 92 | it.id, 93 | it.price, 94 | it.name, 95 | it.description 96 | ) 97 | }.sortedBy { it.price } 98 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_orders.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 28 | 29 | 37 | 38 | 39 | 51 | 52 | 62 | 63 | -------------------------------------------------------------------------------- /app/src/test/java/com/marknkamau/justjava/ui/profile/ProfileViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.profile 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Observer 5 | import com.marknjunge.core.data.model.Resource 6 | import com.marknjunge.core.data.model.User 7 | import com.marknjunge.core.data.repository.AuthRepository 8 | import com.marknjunge.core.data.repository.UsersRepository 9 | import com.marknkamau.justjava.utils.SampleData 10 | import io.mockk.MockKAnnotations 11 | import io.mockk.coEvery 12 | import io.mockk.impl.annotations.MockK 13 | import io.mockk.spyk 14 | import io.mockk.verify 15 | import kotlinx.coroutines.Dispatchers 16 | import kotlinx.coroutines.ExperimentalCoroutinesApi 17 | import kotlinx.coroutines.flow.flow 18 | import kotlinx.coroutines.test.TestCoroutineDispatcher 19 | import kotlinx.coroutines.test.resetMain 20 | import kotlinx.coroutines.test.setMain 21 | import org.junit.After 22 | import org.junit.Before 23 | import org.junit.Rule 24 | import org.junit.Test 25 | import org.junit.rules.TestRule 26 | 27 | class ProfileViewModelTest { 28 | @get:Rule 29 | var rule: TestRule = InstantTaskExecutorRule() 30 | 31 | @ExperimentalCoroutinesApi 32 | private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() 33 | 34 | @MockK 35 | private lateinit var usersRepository: UsersRepository 36 | 37 | @MockK 38 | private lateinit var authRepository: AuthRepository 39 | 40 | private lateinit var viewModel: ProfileViewModel 41 | 42 | @ExperimentalCoroutinesApi 43 | @Before 44 | fun setup() { 45 | MockKAnnotations.init(this, relaxUnitFun = true) 46 | Dispatchers.setMain(testDispatcher) 47 | viewModel = ProfileViewModel(usersRepository, authRepository) 48 | } 49 | 50 | @ExperimentalCoroutinesApi 51 | @After 52 | fun teardown() { 53 | Dispatchers.resetMain() 54 | testDispatcher.cleanupTestCoroutines() 55 | } 56 | 57 | @Test 58 | fun `can get current user`() { 59 | val resource = Resource.Success(SampleData.user) 60 | coEvery { usersRepository.getCurrentUser() } returns flow { emit(resource) } 61 | 62 | val observer = spyk>>() 63 | viewModel.user.observeForever(observer) 64 | viewModel.getCurrentUser() 65 | 66 | verify { observer.onChanged(resource) } 67 | } 68 | 69 | @Test 70 | fun `can update user`() { 71 | val resource = Resource.Success(Unit) 72 | coEvery { usersRepository.updateUser("", "", "", "") } returns resource 73 | 74 | val observer = spyk>>() 75 | viewModel.updateUser("", "", "", "").observeForever(observer) 76 | 77 | verify { observer.onChanged(resource) } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /core/src/main/java/com/marknjunge/core/data/network/NetworkProvider.kt: -------------------------------------------------------------------------------- 1 | package com.marknjunge.core.data.network 2 | 3 | import android.content.Context 4 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory 5 | import com.marknjunge.core.BuildConfig 6 | import com.marknjunge.core.data.local.PreferencesRepository 7 | import com.marknjunge.core.data.network.interceptors.ConvertNoContentInterceptor 8 | import com.marknjunge.core.data.network.interceptors.NetworkConnectionInterceptor 9 | import com.marknjunge.core.data.network.interceptors.SessionIdInterceptor 10 | import com.marknjunge.core.data.network.service.* 11 | import com.marknjunge.core.utils.appJsonConfig 12 | import okhttp3.MediaType.Companion.toMediaType 13 | import okhttp3.OkHttpClient 14 | import okhttp3.logging.HttpLoggingInterceptor 15 | import retrofit2.Retrofit 16 | import java.util.concurrent.TimeUnit 17 | 18 | class NetworkProvider(private val context: Context, private val preferencesRepository: PreferencesRepository) { 19 | private val apiBaseUrl = BuildConfig.API_BASE_URL 20 | private val mediaType = "application/json".toMediaType() 21 | 22 | val apiService: ApiService 23 | val authService: AuthService 24 | val usersService: UsersService 25 | val cartService: CartService 26 | val ordersService: OrdersService 27 | val paymentsService: PaymentsService 28 | 29 | init { 30 | val retrofit = provideRetrofit() 31 | apiService = retrofit.create(ApiService::class.java) 32 | authService = retrofit.create(AuthService::class.java) 33 | usersService = retrofit.create(UsersService::class.java) 34 | cartService = retrofit.create(CartService::class.java) 35 | ordersService = retrofit.create(OrdersService::class.java) 36 | paymentsService = retrofit.create(PaymentsService::class.java) 37 | } 38 | 39 | private fun provideRetrofit(): Retrofit { 40 | return Retrofit.Builder() 41 | .baseUrl(apiBaseUrl) 42 | .addConverterFactory(appJsonConfig.asConverterFactory(mediaType)) 43 | .client(provideOkHttpClient()) 44 | .build() 45 | } 46 | 47 | private fun provideOkHttpClient(): OkHttpClient { 48 | val loggingInterceptor = HttpLoggingInterceptor() 49 | loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY 50 | 51 | val builder = OkHttpClient.Builder() 52 | builder.addInterceptor(NetworkConnectionInterceptor(context)) 53 | builder.addInterceptor(SessionIdInterceptor(preferencesRepository)) 54 | builder.addNetworkInterceptor(ConvertNoContentInterceptor()) 55 | builder.addNetworkInterceptor(loggingInterceptor) 56 | builder.readTimeout(30, TimeUnit.SECONDS) 57 | builder.connectTimeout(30, TimeUnit.SECONDS) 58 | 59 | return builder 60 | .build() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/debug/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 10 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/marknkamau/justjava/ui/orders/OrdersActivity.kt: -------------------------------------------------------------------------------- 1 | package com.marknkamau.justjava.ui.orders 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.activity.viewModels 7 | import androidx.recyclerview.widget.DividerItemDecoration 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.marknjunge.core.data.model.Resource 11 | import com.marknkamau.justjava.databinding.ActivityOrdersBinding 12 | import com.marknkamau.justjava.ui.base.BaseActivity 13 | import com.marknkamau.justjava.ui.orderDetail.OrderDetailActivity 14 | import dagger.hilt.android.AndroidEntryPoint 15 | 16 | @AndroidEntryPoint 17 | class OrdersActivity : BaseActivity() { 18 | 19 | private val ordersViewModel: OrdersViewModel by viewModels() 20 | override var requiresSignedIn = true 21 | private lateinit var binding: ActivityOrdersBinding 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | binding = ActivityOrdersBinding.inflate(layoutInflater) 26 | setContentView(binding.root) 27 | supportActionBar?.title = "Orders" 28 | 29 | observeLoading() 30 | observeOrders() 31 | 32 | ordersViewModel.getOrders() 33 | } 34 | 35 | private fun observeLoading() { 36 | ordersViewModel.loading.observe(this, { loading -> 37 | binding.pbLoading.visibility = if (loading) View.VISIBLE else View.GONE 38 | }) 39 | } 40 | 41 | @SuppressLint("DefaultLocale") 42 | private fun observeOrders() { 43 | val adapter = OrdersAdapter(this) { order -> 44 | OrderDetailActivity.start(this@OrdersActivity, order.id) 45 | } 46 | 47 | binding.rvOrders.stateListAnimator 48 | binding.rvOrders.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false) 49 | val dividerItemDecoration = DividerItemDecoration(this, RecyclerView.VERTICAL) 50 | binding.rvOrders.addItemDecoration(dividerItemDecoration) 51 | binding.rvOrders.adapter = adapter 52 | 53 | ordersViewModel.orders.observe(this, { resource -> 54 | when (resource) { 55 | is Resource.Success -> { 56 | if (resource.data.isEmpty()) { 57 | binding.llNoOrders.visibility = View.VISIBLE 58 | binding.rvOrders.visibility = View.GONE 59 | } else { 60 | binding.llNoOrders.visibility = View.GONE 61 | binding.rvOrders.visibility = View.VISIBLE 62 | adapter.setItems(resource.data.sortedBy { it.datePlaced }.reversed()) 63 | } 64 | } 65 | is Resource.Failure -> handleApiError(resource) 66 | } 67 | }) 68 | } 69 | } 70 | --------------------------------------------------------------------------------