Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Payments and upgrade execution for the course #1

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/src/main/java/org/openedx/app/AnalyticsManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.openedx.app.analytics.SegmentAnalytics
import org.openedx.auth.presentation.AuthAnalytics
import org.openedx.core.config.Config
import org.openedx.core.presentation.CoreAnalytics
import org.openedx.core.presentation.IAPAnalytics
import org.openedx.core.presentation.dialog.appreview.AppReviewAnalytics
import org.openedx.course.presentation.CourseAnalytics
import org.openedx.dashboard.presentation.DashboardAnalytics
Expand All @@ -21,7 +22,7 @@ class AnalyticsManager(
config: Config,
) : AppAnalytics, AppReviewAnalytics, AuthAnalytics, CoreAnalytics, CourseAnalytics,
DashboardAnalytics, DiscoveryAnalytics, DiscussionAnalytics, ProfileAnalytics,
WhatsNewAnalytics {
WhatsNewAnalytics, IAPAnalytics {

private val services: ArrayList<Analytics> = arrayListOf()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package org.openedx.app.data.networking

import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import org.openedx.core.data.model.ErrorResponse
import org.openedx.core.system.EdxError
import okhttp3.Interceptor
import okhttp3.Response
import okio.IOException
import org.openedx.core.data.model.ErrorResponse
import org.openedx.core.system.EdxError

class HandleErrorInterceptor(
private val gson: Gson
Expand All @@ -16,7 +16,7 @@ class HandleErrorInterceptor(

val responseCode = response.code
if (responseCode in 400..500 && response.body != null) {
val jsonStr = response.body!!.string()
val jsonStr = response.peekBody(Long.MAX_VALUE).string()

try {
val errorResponse = gson.fromJson(jsonStr, ErrorResponse::class.java)
Expand All @@ -25,9 +25,11 @@ class HandleErrorInterceptor(
ERROR_INVALID_GRANT -> {
throw EdxError.InvalidGrantException()
}

ERROR_USER_NOT_ACTIVE -> {
throw EdxError.UserNotActiveException()
}

else -> {
return response
}
Expand Down
14 changes: 13 additions & 1 deletion app/src/main/java/org/openedx/app/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.openedx.app.AnalyticsManager
import org.openedx.app.AppAnalytics
import org.openedx.app.deeplink.DeepLinkRouter
import org.openedx.app.AppRouter
import org.openedx.app.BuildConfig
import org.openedx.app.data.storage.PreferencesManager
import org.openedx.app.deeplink.DeepLinkRouter
import org.openedx.app.room.AppDatabase
import org.openedx.app.room.DATABASE_NAME
import org.openedx.auth.presentation.AgreementProvider
Expand All @@ -28,12 +28,15 @@ import org.openedx.auth.presentation.sso.OAuthHelper
import org.openedx.core.ImageProcessor
import org.openedx.core.config.Config
import org.openedx.core.data.model.CourseEnrollments
import org.openedx.core.data.model.CourseStructureModel
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.data.storage.InAppReviewPreferences
import org.openedx.core.module.DownloadWorkerController
import org.openedx.core.module.TranscriptManager
import org.openedx.core.module.billing.BillingProcessor
import org.openedx.core.module.download.FileDownloader
import org.openedx.core.presentation.CoreAnalytics
import org.openedx.core.presentation.IAPAnalytics
import org.openedx.core.presentation.dialog.appreview.AppReviewAnalytics
import org.openedx.core.presentation.dialog.appreview.AppReviewManager
import org.openedx.core.presentation.global.AppData
Expand All @@ -46,6 +49,7 @@ import org.openedx.core.system.connection.NetworkConnection
import org.openedx.core.system.notifier.CourseNotifier
import org.openedx.core.system.notifier.DiscoveryNotifier
import org.openedx.core.system.notifier.DownloadNotifier
import org.openedx.core.system.notifier.IAPNotifier
import org.openedx.core.system.notifier.VideoNotifier
import org.openedx.core.system.notifier.app.AppNotifier
import org.openedx.core.utils.FileUtil
Expand Down Expand Up @@ -88,6 +92,10 @@ val appModule = module {
single<Gson> {
GsonBuilder()
.registerTypeAdapter(CourseEnrollments::class.java, CourseEnrollments.Deserializer())
.registerTypeAdapter(
CourseStructureModel::class.java,
CourseStructureModel.Deserializer(get())
)
.create()
}

Expand All @@ -98,6 +106,7 @@ val appModule = module {
single { DownloadNotifier() }
single { VideoNotifier() }
single { DiscoveryNotifier() }
single { IAPNotifier() }

single { AppRouter() }
single<AuthRouter> { get<AppRouter>() }
Expand Down Expand Up @@ -165,6 +174,8 @@ val appModule = module {
single { WhatsNewManager(get(), get(), get(), get()) }
single<WhatsNewGlobalManager> { get<WhatsNewManager>() }

single<BillingProcessor> { BillingProcessor(get(), get(named("IODispatcher"))) }

single { AnalyticsManager(get(), get()) }
single<AppAnalytics> { get<AnalyticsManager>() }
single<AuthAnalytics> { get<AnalyticsManager>() }
Expand All @@ -176,6 +187,7 @@ val appModule = module {
single<DiscussionAnalytics> { get<AnalyticsManager>() }
single<ProfileAnalytics> { get<AnalyticsManager>() }
single<WhatsNewAnalytics> { get<AnalyticsManager>() }
single<IAPAnalytics> { get<AnalyticsManager>() }

factory { AgreementProvider(get(), get()) }
factory { FacebookAuthHelper() }
Expand Down
13 changes: 12 additions & 1 deletion app/src/main/java/org/openedx/app/di/NetworkingModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.openedx.app.di

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.openedx.app.data.api.NotificationsApi
import org.openedx.app.data.networking.AppUpgradeInterceptor
Expand All @@ -13,6 +14,7 @@ import org.openedx.core.BuildConfig
import org.openedx.core.config.Config
import org.openedx.core.data.api.CookiesApi
import org.openedx.core.data.api.CourseApi
import org.openedx.core.data.api.iap.InAppPurchasesApi
import org.openedx.discovery.data.api.DiscoveryApi
import org.openedx.discussion.data.api.DiscussionApi
import org.openedx.profile.data.api.ProfileApi
Expand Down Expand Up @@ -48,16 +50,25 @@ val networkingModule = module {
.build()
}

single<Retrofit>(named("IAPApiInstance")) {
val config = this.get<Config>()
Retrofit.Builder()
.baseUrl(config.getEcommerceURL())
.client(get())
.addConverterFactory(GsonConverterFactory.create(get()))
.build()
}

single { provideApi<AuthApi>(get()) }
single { provideApi<CookiesApi>(get()) }
single { provideApi<CourseApi>(get()) }
single { provideApi<ProfileApi>(get()) }
single { provideApi<DiscussionApi>(get()) }
single { provideApi<DiscoveryApi>(get()) }
single { provideApi<NotificationsApi>(get()) }
single { provideApi<InAppPurchasesApi>(get(named("IAPApiInstance"))) }
}


inline fun <reified T> provideApi(retrofit: Retrofit): T {
return retrofit.create(T::class.java)
}
43 changes: 42 additions & 1 deletion app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import org.openedx.auth.presentation.restore.RestorePasswordViewModel
import org.openedx.auth.presentation.signin.SignInViewModel
import org.openedx.auth.presentation.signup.SignUpViewModel
import org.openedx.core.Validator
import org.openedx.core.data.repository.iap.IAPRepository
import org.openedx.core.domain.interactor.IAPInteractor
import org.openedx.core.domain.model.iap.PurchaseFlowData
import org.openedx.core.presentation.dialog.selectorbottomsheet.SelectDialogViewModel
import org.openedx.core.presentation.iap.IAPFlow
import org.openedx.core.presentation.iap.IAPViewModel
import org.openedx.core.presentation.settings.video.VideoQualityViewModel
import org.openedx.core.ui.WindowSize
import org.openedx.course.data.repository.CourseRepository
Expand Down Expand Up @@ -133,7 +138,23 @@ val screenModule = module {

factory { DashboardRepository(get(), get(), get(), get()) }
factory { DashboardInteractor(get()) }
viewModel { DashboardListViewModel(get(), get(), get(), get(), get(), get(), get()) }
viewModel {
DashboardListViewModel(
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get()
)
}

viewModel { (windowSize: WindowSize) ->
DashboardGalleryViewModel(
get(),
Expand Down Expand Up @@ -190,6 +211,8 @@ val screenModule = module {
get(),
get(),
get(),
get(),
get(),
get()
)
}
Expand Down Expand Up @@ -240,6 +263,8 @@ val screenModule = module {
get(),
get(),
get(),
get(),
get(),
get()
)
}
Expand Down Expand Up @@ -415,6 +440,22 @@ val screenModule = module {
)
}

single { IAPRepository(get()) }
factory { IAPInteractor(get(), get()) }
viewModel { (iapFlow: IAPFlow, purchaseFlowData: PurchaseFlowData) ->
IAPViewModel(
iapFlow = iapFlow,
purchaseFlowData = purchaseFlowData,
get(),
get(),
get(),
get(),
get(),
get(),
get()
)
}

viewModel { (descendants: List<String>) ->
DownloadQueueViewModel(
descendants,
Expand Down
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ ext {

webkit_version = "1.11.0"

billing_version = "6.2.1"

configHelper = new ConfigHelper(projectDir, getCurrentFlavor())

//testing
Expand Down
3 changes: 3 additions & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ dependencies {
api "com.google.android.gms:play-services-ads-identifier:18.0.1"
api "com.android.installreferrer:installreferrer:2.2"

// Google Play Billing Library
api "com.android.billingclient:billing-ktx:$billing_version"

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/java/org/openedx/core/ApiConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ object ApiConstants {
const val HONOR_CODE = "honor_code"
const val MARKETING_EMAILS = "marketing_emails_opt_in"
}

object IAPFields {
const val PAYMENT_PROCESSOR = "android-iap"
}
}
5 changes: 5 additions & 0 deletions core/src/main/java/org/openedx/core/config/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class Config(context: Context) {
return getString(API_HOST_URL)
}

fun getEcommerceURL(): String {
HamzaIsrar12 marked this conversation as resolved.
Show resolved Hide resolved
return getString(ECOMMERCE_URL, "")
}

fun getUriScheme(): String {
return getString(URI_SCHEME)
}
Expand Down Expand Up @@ -152,6 +156,7 @@ class Config(context: Context) {
companion object {
private const val APPLICATION_ID = "APPLICATION_ID"
private const val API_HOST_URL = "API_HOST_URL"
private const val ECOMMERCE_URL = "ECOMMERCE_URL"
private const val URI_SCHEME = "URI_SCHEME"
private const val OAUTH_CLIENT_ID = "OAUTH_CLIENT_ID"
private const val TOKEN_TYPE = "TOKEN_TYPE"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.openedx.core.data.api.iap

import org.openedx.core.data.model.iap.AddToBasketResponse
import org.openedx.core.data.model.iap.CheckoutResponse
import org.openedx.core.data.model.iap.ExecuteOrderResponse
import retrofit2.Response
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query

interface InAppPurchasesApi {
@GET("/api/iap/v1/basket/add/")
suspend fun addToBasket(@Query("sku") productId: String): Response<AddToBasketResponse>

@FormUrlEncoded
@POST("/api/iap/v1/checkout/")
suspend fun proceedCheckout(
@Field("basket_id") basketId: Long,
@Field("payment_processor") paymentProcessor: String
): Response<CheckoutResponse>

@FormUrlEncoded
@POST("/api/iap/v1/execute/")
suspend fun executeOrder(
@Field("basket_id") basketId: Long,
@Field("payment_processor") paymentProcessor: String,
@Field("purchase_token") purchaseToken: String,
@Field("price") price: Double,
@Field("currency_code") currencyCode: String,
): Response<ExecuteOrderResponse>
}
8 changes: 8 additions & 0 deletions core/src/main/java/org/openedx/core/data/model/AppConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ import org.openedx.core.domain.model.AppConfig as DomainAppConfig
data class AppConfig(
@SerializedName("course_dates_calendar_sync")
val calendarSyncConfig: CalendarSyncConfig = CalendarSyncConfig(),

@SerializedName("value_prop_enabled")
val isValuePropEnabled: Boolean = false,

@SerializedName("iap_config")
val iapConfig: IAPConfig = IAPConfig(),
) {
fun mapToDomain(): DomainAppConfig {
return DomainAppConfig(
courseDatesCalendarSync = calendarSyncConfig.mapToDomain(),
isValuePropEnabled = isValuePropEnabled,
iapConfig = iapConfig.mapToDomain(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.openedx.core.data.model

import com.google.gson.annotations.SerializedName
import org.openedx.core.data.model.room.discovery.CourseAccessDetailsDb
import org.openedx.core.utils.TimeUtils
import org.openedx.core.domain.model.CourseAccessDetails as DomainCourseAccessDetails

data class CourseAccessDetails(
@SerializedName("audit_access_expires")
val auditAccessExpires: String?,
@SerializedName("courseware_access")
var coursewareAccess: CoursewareAccess?,
) {
fun mapToDomain(): DomainCourseAccessDetails =
DomainCourseAccessDetails(
TimeUtils.iso8601ToDate(auditAccessExpires ?: ""),
coursewareAccess?.mapToDomain()
)

fun mapToRoomEntity(): CourseAccessDetailsDb =
CourseAccessDetailsDb(auditAccessExpires, coursewareAccess?.mapToRoomEntity())
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ data class CourseEnrollments(
val appConfig = deserializeAppConfig(json)
val primaryCourse = deserializePrimaryCourse(json)

if (appConfig.iapConfig.productPrefix.isNotEmpty()) {
enrollments.results.forEach { courseData ->
courseData.setStoreSku(appConfig.iapConfig.productPrefix)
}
}

return CourseEnrollments(enrollments, appConfig, primaryCourse)
}

Expand Down
Loading
Loading