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