diff --git a/app/build.gradle b/app/build.gradle index a168b150927ee68a3a1c91b224464313dba0c1c3..0ee765ad9d6a1c0668f71fb5339a0dbaf5ee0d7b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -79,20 +79,17 @@ dependencies { //Kotlin implementation "androidx.core:core-ktx:1.8.0" - //Lifecycle - def lifecycle_version = "2.5.0" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + //androidx + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.ext.androidx_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.ext.androidx_version" + implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.ext.androidx_version" + implementation "androidx.navigation:navigation-ui-ktx:$rootProject.ext.androidx_version" //Koin def koin_version = "3.2.0" implementation "io.insert-koin:koin-core:$koin_version" implementation "io.insert-koin:koin-android:$koin_version" - //Navigation - implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.ext.nav_version" - implementation "androidx.navigation:navigation-ui-ktx:$rootProject.ext.nav_version" - //ViewBinding implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6' @@ -114,10 +111,13 @@ dependencies { //ExoPlayer implementation 'com.google.android.exoplayer:exoplayer:2.18.1' - //Firebase crash reports for gplay flavor + //Firebase crash reports gplayImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.2.12' gplayImplementation 'com.google.firebase:firebase-analytics-ktx:21.1.0' + //Subscriptions + gplayImplementation 'com.android.billingclient:billing-ktx:5.0.0' + //test testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' diff --git a/app/src/fdroid/java/org/futo/circles/subscriptions/SubscriptionManagerProvider.kt b/app/src/fdroid/java/org/futo/circles/subscriptions/SubscriptionManagerProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d68ea0a6a81e895668ec274ccce593d21f7f5c8 --- /dev/null +++ b/app/src/fdroid/java/org/futo/circles/subscriptions/SubscriptionManagerProvider.kt @@ -0,0 +1,13 @@ +package org.futo.circles.subscriptions + +import android.app.Activity +import org.futo.circles.R + +object SubscriptionManagerProvider : SubscriptionProvider { + + override fun getManager( + activity: Activity, + itemPurchaseListener: ItemPurchasedListener + ): SubscriptionManager = + throw IllegalStateException(activity.getString(R.string.flavour_does_not_support_subscriptions)) +} \ No newline at end of file diff --git a/app/src/gplay/java/org/futo/circles/subscriptions/SubscriptionManagerProvider.kt b/app/src/gplay/java/org/futo/circles/subscriptions/SubscriptionManagerProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..370d6496b69a405a359530a1928577a214ca4ee4 --- /dev/null +++ b/app/src/gplay/java/org/futo/circles/subscriptions/SubscriptionManagerProvider.kt @@ -0,0 +1,12 @@ +package org.futo.circles.subscriptions + +import android.app.Activity +import org.futo.circles.subscriptions.google.GoogleSubscriptionsManager + +object SubscriptionManagerProvider : SubscriptionProvider { + + override fun getManager( + activity: Activity, + itemPurchaseListener: ItemPurchasedListener + ): SubscriptionManager = GoogleSubscriptionsManager(activity, itemPurchaseListener) +} \ No newline at end of file diff --git a/app/src/gplay/java/org/futo/circles/subscriptions/google/GoogleSubscriptionsManager.kt b/app/src/gplay/java/org/futo/circles/subscriptions/google/GoogleSubscriptionsManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..f8e8330d3260bf6a6329f4d114c142a87063682c --- /dev/null +++ b/app/src/gplay/java/org/futo/circles/subscriptions/google/GoogleSubscriptionsManager.kt @@ -0,0 +1,154 @@ +package org.futo.circles.subscriptions.google + +import android.app.Activity +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.android.billingclient.api.* +import com.android.billingclient.api.BillingClient.BillingResponseCode.* +import kotlinx.coroutines.suspendCancellableCoroutine +import org.futo.circles.R +import org.futo.circles.extensions.Response +import org.futo.circles.extensions.onBG +import org.futo.circles.model.SubscriptionListItem +import org.futo.circles.subscriptions.ItemPurchasedListener +import org.futo.circles.subscriptions.SubscriptionManager +import kotlin.coroutines.resume + +class GoogleSubscriptionsManager( + private val activity: Activity, + private val itemPurchasedListener: ItemPurchasedListener +) : SubscriptionManager { + + + private val purchasesUpdatedListener = + PurchasesUpdatedListener { billingResult, purchases -> + purchases?.let { + if (billingResult.responseCode == OK) + itemPurchasedListener.onItemPurchased( + purchases.lastOrNull()?.originalJson ?: "" + ) + else itemPurchasedListener.onPurchaseFailed(billingResult.responseCode) + } + } + + private val client = BillingClient.newBuilder(activity) + .setListener(purchasesUpdatedListener) + .enablePendingPurchases() + .build() + + + init { + (activity as? AppCompatActivity)?.lifecycle?.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + client.endConnection() + super.onDestroy(owner) + } + }) + } + + override suspend fun getDetails(productIds: List<String>): Response<List<SubscriptionListItem>> = + when (val code = + client.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode) { + OK -> queryDetails(productIds).toSubscriptionListItemsResponse(activity.applicationContext) + SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE, BILLING_UNAVAILABLE -> onBG { + tryConnectAndDo { queryDetails(productIds).toSubscriptionListItemsResponse(activity.applicationContext) } + } + else -> getErrorResponseForCode(code) + } + + + override suspend fun purchaseProduct(productId: String): Response<Unit> { + val detailsResponse = queryDetails(listOf(productId)) + val productDetails = + (detailsResponse as? Response.Success)?.data?.firstOrNull { it.productId == productId } + ?: return getErrorResponseForCode(ERROR) + + return when (val code = client + .isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode) { + OK -> purchase(productDetails) + SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE, BILLING_UNAVAILABLE -> onBG { + tryConnectAndDo { purchase(productDetails) } + } + ITEM_ALREADY_OWNED -> Response.Success(Unit) + else -> getErrorResponseForCode(code) + } + } + + private suspend inline fun <T> tryConnectAndDo(action: () -> Response<T>): Response<T> = + when (val connectResult = client.tryConnect()) { + is Response.Success -> if (connectResult.data) action() + else getErrorResponseForCode(SERVICE_DISCONNECTED) + is Response.Error -> connectResult + } + + + private suspend fun BillingClient.tryConnect(): Response<Boolean> = + suspendCancellableCoroutine { continuation -> + startConnection(object : BillingClientStateListener { + override fun onBillingSetupFinished(billingResult: com.android.billingclient.api.BillingResult) { + if (continuation.isCancelled || continuation.isCompleted) return + continuation.resume( + when (billingResult.responseCode) { + OK -> Response.Success(true) + else -> getErrorResponseForCode(billingResult.responseCode) + } + ) + } + + override fun onBillingServiceDisconnected() { + if (continuation.isCancelled || continuation.isCompleted) return + continuation.resume(getErrorResponseForCode(SERVICE_DISCONNECTED)) + } + }) + } + + private suspend fun queryDetails(productIds: List<String>): Response<List<ProductDetails>> { + val params = QueryProductDetailsParams.newBuilder() + .setProductList(productIds.map { + QueryProductDetailsParams.Product.newBuilder() + .setProductId(it) + .setProductType(BillingClient.ProductType.SUBS) + .build() + }) + + val productDetailsResult = onBG { client.queryProductDetails(params.build()) } + + return if (productDetailsResult.billingResult.responseCode == OK) { + productDetailsResult.productDetailsList?.filter { productIds.contains(it.productId) } + ?.takeIf { it.isNotEmpty() } + ?.let { Response.Success(data = it) } + ?: getErrorResponseForCode(code = ERROR) + } else { + getErrorResponseForCode(productDetailsResult.billingResult.responseCode) + } + } + + private fun purchase(productDetails: ProductDetails): Response<Unit> { + val productDetailsParamsList = listOf( + BillingFlowParams.ProductDetailsParams.newBuilder() + .setProductDetails(productDetails) + .build() + ) + + val billingFlowParams = BillingFlowParams.newBuilder() + .setProductDetailsParamsList(productDetailsParamsList) + .build() + + return when (val code = + client.launchBillingFlow(activity, billingFlowParams).responseCode) { + OK -> Response.Success(Unit) + else -> getErrorResponseForCode(code) + } + } + + private fun getErrorResponseForCode(code: Int) = when (code) { + FEATURE_NOT_SUPPORTED -> Response.Error(activity.getString(R.string.feature_not_supported)) + SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE, BILLING_UNAVAILABLE -> Response.Error(activity.getString(R.string.service_unavailable)) + ITEM_UNAVAILABLE -> Response.Error(activity.getString(R.string.item_unavailable)) + USER_CANCELED -> Response.Error(activity.getString(R.string.user_canceled)) + ITEM_NOT_OWNED -> Response.Error(activity.getString(R.string.item_not_owned)) + DEVELOPER_ERROR -> Response.Error(activity.getString(R.string.developer_error)) + else -> Response.Error(activity.getString(R.string.purchase_failed_format, code)) + } +} \ No newline at end of file diff --git a/app/src/gplay/java/org/futo/circles/subscriptions/google/ProductDetailsMapping.kt b/app/src/gplay/java/org/futo/circles/subscriptions/google/ProductDetailsMapping.kt new file mode 100644 index 0000000000000000000000000000000000000000..29d5a9f50c68bbf2544bd1aef8d9967e2c17bf27 --- /dev/null +++ b/app/src/gplay/java/org/futo/circles/subscriptions/google/ProductDetailsMapping.kt @@ -0,0 +1,28 @@ +package org.futo.circles.subscriptions.google + +import android.content.Context +import com.android.billingclient.api.ProductDetails +import org.futo.circles.extensions.Response +import org.futo.circles.model.SubscriptionListItem +import org.futo.circles.subscriptions.formatIsoPeriod + +fun ProductDetails.toSubscriptionListItem(context: Context): SubscriptionListItem { + val productOffer = + subscriptionOfferDetails?.last()?.pricingPhases?.pricingPhaseList?.last() + + return SubscriptionListItem( + id = productId, + name = name, + description = description, + price = productOffer?.formattedPrice ?: "", + duration = productOffer?.billingPeriod?.formatIsoPeriod(context) ?: "" + ) +} + +fun Response<List<ProductDetails>>.toSubscriptionListItemsResponse(context: Context): Response<List<SubscriptionListItem>> = + when (val response = this) { + is Response.Success -> Response.Success(response.data.map { + it.toSubscriptionListItem(context) + }) + is Response.Error -> response + } \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/core/Constants.kt b/app/src/main/java/org/futo/circles/core/Constants.kt index b6c5ad8b05fe5b89ee83d63722751142640ff20a..eaff532c22140cf34e025fe08957be7fd9189c0a 100644 --- a/app/src/main/java/org/futo/circles/core/Constants.kt +++ b/app/src/main/java/org/futo/circles/core/Constants.kt @@ -2,7 +2,8 @@ package org.futo.circles.core const val FILE_PROVIDER_AUTHORITY_PREFIX = ".provider" -const val REGISTRATION_TOKEN_KEY_PREFIX = "login.registration_token" +const val REGISTRATION_TOKEN_KEY_PREFIX = "registration_token" +const val REGISTRATION_SUBSCRIPTION_KEY_PREFIX = "subscription.google" const val TERMS_URL_EXTENSION = "_matrix/consent" const val VALIDATION_TOKEN_SUBMIT_URL_PREFIX = "_matrix/identity/api/v1/validate/email/submitToken" diff --git a/app/src/main/java/org/futo/circles/di/DataSourceModule.kt b/app/src/main/java/org/futo/circles/di/DataSourceModule.kt index c33c38e6683924e83dd13eef53d1992268699f7f..b44ca7940aab3a09488440fb1e46e55b90c979eb 100644 --- a/app/src/main/java/org/futo/circles/di/DataSourceModule.kt +++ b/app/src/main/java/org/futo/circles/di/DataSourceModule.kt @@ -31,6 +31,7 @@ import org.futo.circles.feature.sign_up.SignUpDataSource import org.futo.circles.feature.sign_up.setup_circles.SetupCirclesDataSource import org.futo.circles.feature.sign_up.setup_profile.SetupProfileDataSource import org.futo.circles.feature.sign_up.sign_up_type.SelectSignUpTypeDataSource +import org.futo.circles.feature.sign_up.subscription_stage.SubscriptionStageDataSource import org.futo.circles.feature.sign_up.terms.AcceptTermsDataSource import org.futo.circles.feature.sign_up.validate_email.ValidateEmailDataSource import org.futo.circles.feature.sign_up.validate_token.ValidateTokenDataSource @@ -92,4 +93,5 @@ val dataSourceModule = module { factory { UserOptionsDataSource() } factory { (userId: String) -> UserDataSource(get(), userId) } single { PickDeviceMediaDataSource(get()) } + factory { SubscriptionStageDataSource(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/di/UiModule.kt b/app/src/main/java/org/futo/circles/di/UiModule.kt index fb43d9535d3fe0aebaa5aff01f779c5fd4b30963..e68fdd6476efeff7c1f4c8bb2c1af778d6bf3294 100644 --- a/app/src/main/java/org/futo/circles/di/UiModule.kt +++ b/app/src/main/java/org/futo/circles/di/UiModule.kt @@ -28,6 +28,7 @@ import org.futo.circles.feature.sign_up.SignUpViewModel import org.futo.circles.feature.sign_up.setup_circles.SetupCirclesViewModel import org.futo.circles.feature.sign_up.setup_profile.SetupProfileViewModel import org.futo.circles.feature.sign_up.sign_up_type.SelectSignUpTypeViewModel +import org.futo.circles.feature.sign_up.subscription_stage.SubscriptionStageViewModel import org.futo.circles.feature.sign_up.terms.AcceptTermsViewModel import org.futo.circles.feature.sign_up.validate_email.ValidateEmailViewModel import org.futo.circles.feature.sign_up.validate_token.ValidateTokenViewModel @@ -102,4 +103,5 @@ val uiModule = module { viewModel { (isVideoAvailable: Boolean) -> PickDeviceMediaViewModel(isVideoAvailable, get()) } + viewModel { SubscriptionStageViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/feature/sign_up/SignUpDataSource.kt b/app/src/main/java/org/futo/circles/feature/sign_up/SignUpDataSource.kt index e237053c929544f27067409a7d662d77d1113847..ab7447e322916a8f891df8adacb039ca801d20eb 100644 --- a/app/src/main/java/org/futo/circles/feature/sign_up/SignUpDataSource.kt +++ b/app/src/main/java/org/futo/circles/feature/sign_up/SignUpDataSource.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import org.futo.circles.R +import org.futo.circles.core.REGISTRATION_SUBSCRIPTION_KEY_PREFIX import org.futo.circles.core.REGISTRATION_TOKEN_KEY_PREFIX import org.futo.circles.core.SingleEventLiveData import org.futo.circles.core.matrix.pass_phrase.create.CreatePassPhraseDataSource @@ -18,7 +19,7 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.session.Session -enum class NavigationEvents { TokenValidation, AcceptTerm, ValidateEmail } +enum class NavigationEvents { TokenValidation, Subscription, AcceptTerm, ValidateEmail } class SignUpDataSource( private val context: Context, @@ -37,6 +38,8 @@ class SignUpDataSource( private set var currentHomeServerUrl: String = "" + private set + private var passphrase: String = "" private var userName: String = "" @@ -44,7 +47,8 @@ class SignUpDataSource( stages: List<Stage>, name: String, password: String, - homeServerUrl: String + homeServerUrl: String, + isSubscription: Boolean ) { currentStage = null stagesToComplete.clear() @@ -52,7 +56,7 @@ class SignUpDataSource( passphrase = password currentHomeServerUrl = homeServerUrl - stagesToComplete.addAll(stages.filter { it.mandatory }) + setupStages(stages, isSubscription) navigateToNextStage() } @@ -66,6 +70,16 @@ class SignUpDataSource( subtitleLiveData.postValue("") } + private fun setupStages(stages: List<Stage>, isSubscription: Boolean) { + val otherStages = stages.filterIsInstance<Stage.Other>() + val firstStage = otherStages.firstOrNull { + if (isSubscription) it.type.endsWith(REGISTRATION_SUBSCRIPTION_KEY_PREFIX) + else it.type.endsWith(REGISTRATION_TOKEN_KEY_PREFIX) + } ?: throw IllegalArgumentException(context.getString(R.string.wrong_signup_config)) + stagesToComplete.add(firstStage) + stagesToComplete.addAll(stages.filter { it.mandatory }) + } + private suspend fun finishRegistration(session: Session) = createResult { MatrixInstanceProvider.matrix.authenticationService().reset() MatrixSessionProvider.awaitForSessionStart(session) @@ -103,11 +117,11 @@ class SignUpDataSource( private fun handleStageOther(type: String): NavigationEvents = if (type.endsWith(REGISTRATION_TOKEN_KEY_PREFIX)) NavigationEvents.TokenValidation + else if (type.endsWith(REGISTRATION_SUBSCRIPTION_KEY_PREFIX)) NavigationEvents.Subscription else throw IllegalArgumentException( context.getString(R.string.not_supported_stage_format, type) ) - private fun updatePageSubtitle() { val size = stagesToComplete.size val number = getCurrentStageIndex() + 1 @@ -115,5 +129,7 @@ class SignUpDataSource( subtitleLiveData.postValue(subtitle) } - + companion object { + const val TYPE_PARAM_KEY = "type" + } } \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/feature/sign_up/SignUpFragment.kt b/app/src/main/java/org/futo/circles/feature/sign_up/SignUpFragment.kt index 4138c330639ed11dbde2058e23631c37c0548fae..ffa334408ae71fe1e96af5df887f9bbca046ad13 100644 --- a/app/src/main/java/org/futo/circles/feature/sign_up/SignUpFragment.kt +++ b/app/src/main/java/org/futo/circles/feature/sign_up/SignUpFragment.kt @@ -57,6 +57,7 @@ class SignUpFragment : Fragment(R.layout.sign_up_fragment), BackPressOwner { private fun handleNavigation(event: NavigationEvents) { val directionId = when (event) { NavigationEvents.TokenValidation -> R.id.to_validateToken + NavigationEvents.Subscription -> R.id.to_subscriptions NavigationEvents.AcceptTerm -> R.id.to_acceptTerms NavigationEvents.ValidateEmail -> R.id.to_validateEmail } diff --git a/app/src/main/java/org/futo/circles/feature/sign_up/sign_up_type/SelectSignUpTypeDataSource.kt b/app/src/main/java/org/futo/circles/feature/sign_up/sign_up_type/SelectSignUpTypeDataSource.kt index 9cdb9cfe8b7d138d5f7da19e010ebe996bccda1d..63752002be30deadb01b432397237ca477424dec 100644 --- a/app/src/main/java/org/futo/circles/feature/sign_up/sign_up_type/SelectSignUpTypeDataSource.kt +++ b/app/src/main/java/org/futo/circles/feature/sign_up/sign_up_type/SelectSignUpTypeDataSource.kt @@ -21,7 +21,12 @@ class SelectSignUpTypeDataSource( signUpDataSource.clearSubtitle() } - suspend fun startNewRegistration(name: String, password: String, domain: String) = + suspend fun startNewRegistration( + name: String, + password: String, + domain: String, + isSubscription: Boolean + ) = createResult { val homeServerUrl = HomeServerUtils.getHomeServerUrlFromDomain(domain) authService.cancelPendingLoginOrRegistration() @@ -39,7 +44,8 @@ class SelectSignUpTypeDataSource( it.flowResult.missingStages, name, password, - homeServerUrl + homeServerUrl, + isSubscription ) } } diff --git a/app/src/main/java/org/futo/circles/feature/sign_up/sign_up_type/SelectSignUpTypeFragment.kt b/app/src/main/java/org/futo/circles/feature/sign_up/sign_up_type/SelectSignUpTypeFragment.kt index a1f11d72578e481db9713b9385c7b1b106d0fc5c..0f4512417d049486528f704f012c12ede0da505f 100644 --- a/app/src/main/java/org/futo/circles/feature/sign_up/sign_up_type/SelectSignUpTypeFragment.kt +++ b/app/src/main/java/org/futo/circles/feature/sign_up/sign_up_type/SelectSignUpTypeFragment.kt @@ -26,15 +26,13 @@ class SelectSignUpTypeFragment : Fragment(R.layout.select_sign_up_type_fragment) viewModel.clearSubtitle() setupViews() setupObservers() - } private fun setupViews() { - setAlwaysDisabledViews(listOf(binding.btnSubscription)) with(binding) { groupSubscription.setIsVisible(BuildConfig.IS_SUBSCRIPTIONS_ENABLED) - tilUserName.editText?.doAfterTextChanged { setTokenButtonEnabled() } - tilPassword.editText?.doAfterTextChanged { setTokenButtonEnabled() } + tilUserName.editText?.doAfterTextChanged { setSignupButtonsEnabled() } + tilPassword.editText?.doAfterTextChanged { setSignupButtonsEnabled() } btnToken.setOnClickListener { startLoading(btnToken) viewModel.startSignUp( @@ -43,6 +41,15 @@ class SelectSignUpTypeFragment : Fragment(R.layout.select_sign_up_type_fragment) tvServerDomain.text.toString() ) } + btnSubscription.setOnClickListener { + startLoading(btnSubscription) + viewModel.startSignUp( + tilUserName.getText(), + tilPassword.getText(), + tvServerDomain.text.toString(), + true + ) + } serverLocationGroup.setOnCheckedChangeListener { _, checkedId -> tvServerDomain.text = when (checkedId) { btnUS.id -> BuildConfig.US_SERVER_DOMAIN @@ -58,8 +65,10 @@ class SelectSignUpTypeFragment : Fragment(R.layout.select_sign_up_type_fragment) viewModel.startSignUpEventLiveData.observeResponse(this) } - private fun setTokenButtonEnabled() { - binding.btnToken.isEnabled = binding.tilUserName.editText?.text?.isNotEmpty() == true && + private fun setSignupButtonsEnabled() { + val isEnabled = binding.tilUserName.editText?.text?.isNotEmpty() == true && binding.tilPassword.editText?.text?.isNotEmpty() == true + binding.btnToken.isEnabled = isEnabled + binding.btnSubscription.isEnabled = isEnabled } } \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/feature/sign_up/sign_up_type/SelectSignUpTypeViewModel.kt b/app/src/main/java/org/futo/circles/feature/sign_up/sign_up_type/SelectSignUpTypeViewModel.kt index e9874360b86cd2cf0f16988f9959f1d3316a6a70..3979b0544d8d015f005da5688ab1a9cd7362e2ca 100644 --- a/app/src/main/java/org/futo/circles/feature/sign_up/sign_up_type/SelectSignUpTypeViewModel.kt +++ b/app/src/main/java/org/futo/circles/feature/sign_up/sign_up_type/SelectSignUpTypeViewModel.kt @@ -11,10 +11,15 @@ class SelectSignUpTypeViewModel( val startSignUpEventLiveData = SingleEventLiveData<Response<Unit?>>() - fun startSignUp(name: String, password: String, serverDomain: String) { + fun startSignUp( + name: String, + password: String, + serverDomain: String, + isSubscription: Boolean = false + ) { launchBg { startSignUpEventLiveData.postValue( - dataSource.startNewRegistration(name, password, serverDomain) + dataSource.startNewRegistration(name, password, serverDomain, isSubscription) ) } } diff --git a/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/SubscriptionStageDataSource.kt b/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/SubscriptionStageDataSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..3c79e48171a2f8527d2fbf8afd271237346aa9c4 --- /dev/null +++ b/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/SubscriptionStageDataSource.kt @@ -0,0 +1,45 @@ +package org.futo.circles.feature.sign_up.subscription_stage + +import org.futo.circles.extensions.Response +import org.futo.circles.extensions.createResult +import org.futo.circles.feature.sign_up.SignUpDataSource +import org.futo.circles.feature.sign_up.SignUpDataSource.Companion.TYPE_PARAM_KEY +import org.futo.circles.provider.MatrixInstanceProvider +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.auth.registration.Stage + +class SubscriptionStageDataSource( + private val signUpDataSource: SignUpDataSource +) { + + private val wizard by lazy { + MatrixInstanceProvider.matrix.authenticationService().getRegistrationWizard() + } + + suspend fun validateSubscriptionReceipt(receipt: String): Response<RegistrationResult> { + val type = (signUpDataSource.currentStage as? Stage.Other)?.type ?: "" + + val result = createResult { + wizard.registrationCustom( + mapOf( + TYPE_PARAM_KEY to type, + PRODUCT_PARAM_KEY to receipt + ) + ) + } + + (result as? Response.Success)?.let { signUpDataSource.stageCompleted(result.data) } + return result + } + + fun getProductIdsList() = ((signUpDataSource.currentStage as? Stage.Other) + ?.params?.get(PRODUCT_IDS_KEY) as? List<*>) + ?.map { it.toString() } + ?: emptyList() + + + companion object { + private const val PRODUCT_PARAM_KEY = "product" + private const val PRODUCT_IDS_KEY = "productIds" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/SubscriptionStageFragment.kt b/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/SubscriptionStageFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..76cec72b3b5aff978d28645e7576204e55c255b3 --- /dev/null +++ b/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/SubscriptionStageFragment.kt @@ -0,0 +1,66 @@ +package org.futo.circles.feature.sign_up.subscription_stage + +import android.os.Bundle +import android.view.View +import by.kirich1409.viewbindingdelegate.viewBinding +import org.futo.circles.R +import org.futo.circles.core.fragment.ParentBackPressOwnerFragment +import org.futo.circles.databinding.SubscriptionStageFragmentBinding +import org.futo.circles.extensions.observeResponse +import org.futo.circles.extensions.showError +import org.futo.circles.feature.sign_up.subscription_stage.list.SubscriptionsAdapter +import org.futo.circles.subscriptions.ItemPurchasedListener +import org.futo.circles.subscriptions.SubscriptionManagerProvider +import org.koin.androidx.viewmodel.ext.android.viewModel + +class SubscriptionStageFragment : + ParentBackPressOwnerFragment(R.layout.subscription_stage_fragment) { + + private val binding by viewBinding(SubscriptionStageFragmentBinding::bind) + private val viewModel by viewModel<SubscriptionStageViewModel>() + + private val subscriptionManager by lazy { + SubscriptionManagerProvider.getManager( + requireActivity(), object : ItemPurchasedListener { + override fun onItemPurchased(purchase: String) { + viewModel.validateSubscriptionReceipt(purchase) + } + + override fun onPurchaseFailed(errorCode: Int) { + showError(getString(R.string.purchase_failed_format, errorCode)) + } + } + ) + } + + private val listAdapter by lazy { + SubscriptionsAdapter(onItemClicked = { id -> onSubscriptionSelected(id) }) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel.loadSubscriptionsList(subscriptionManager) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupViews() + setupObservers() + } + + private fun setupViews() { + binding.rvSubscriptions.adapter = listAdapter + } + + private fun setupObservers() { + viewModel.subscribeLiveData.observeResponse(this) + viewModel.purchaseLiveData.observeResponse(this) + viewModel.subscriptionsListLiveData.observeResponse(this, success = { + listAdapter.submitList(it) + }) + } + + private fun onSubscriptionSelected(productId: String) { + viewModel.purchaseProduct(subscriptionManager, productId) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/SubscriptionStageViewModel.kt b/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/SubscriptionStageViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..b0da6cb95836f02976e75e41ff34f3faf7123ffb --- /dev/null +++ b/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/SubscriptionStageViewModel.kt @@ -0,0 +1,39 @@ +package org.futo.circles.feature.sign_up.subscription_stage + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.futo.circles.core.SingleEventLiveData +import org.futo.circles.extensions.Response +import org.futo.circles.extensions.launchBg +import org.futo.circles.extensions.launchUi +import org.futo.circles.model.SubscriptionListItem +import org.futo.circles.subscriptions.SubscriptionManager +import org.matrix.android.sdk.api.auth.registration.RegistrationResult + +class SubscriptionStageViewModel( + private val dataSource: SubscriptionStageDataSource +) : ViewModel() { + + val subscribeLiveData = SingleEventLiveData<Response<RegistrationResult>>() + val purchaseLiveData = SingleEventLiveData<Response<Unit>>() + val subscriptionsListLiveData = MutableLiveData<Response<List<SubscriptionListItem>>>() + + fun validateSubscriptionReceipt(receipt: String) { + launchBg { + subscribeLiveData.postValue(dataSource.validateSubscriptionReceipt(receipt)) + } + } + + fun loadSubscriptionsList(subscriptionManager: SubscriptionManager) { + launchBg { + subscriptionsListLiveData.postValue(subscriptionManager.getDetails(dataSource.getProductIdsList())) + } + } + + fun purchaseProduct(subscriptionManager: SubscriptionManager, productId: String) { + launchUi { + purchaseLiveData.postValue(subscriptionManager.purchaseProduct(productId)) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/list/SubscriptionViewHolder.kt b/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/list/SubscriptionViewHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..d8c4f7d891b3823acb9c317641e20a0b54fcd98d --- /dev/null +++ b/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/list/SubscriptionViewHolder.kt @@ -0,0 +1,32 @@ +package org.futo.circles.feature.sign_up.subscription_stage.list + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.futo.circles.core.list.ViewBindingHolder +import org.futo.circles.databinding.SubscriptionListItemBinding +import org.futo.circles.extensions.onClick +import org.futo.circles.model.SubscriptionListItem + + +class SubscriptionViewHolder( + parent: ViewGroup, + private val onSubscriptionClicked: (Int) -> Unit +) : RecyclerView.ViewHolder(inflate(parent, SubscriptionListItemBinding::inflate)) { + + private companion object : ViewBindingHolder + + private val binding = baseBinding as SubscriptionListItemBinding + + init { + onClick(itemView) { position -> onSubscriptionClicked(position) } + } + + fun bind(data: SubscriptionListItem) { + with(binding) { + tvName.text = data.name + tvDetails.text = data.description + tvPrice.text = data.price + tvDuration.text = data.duration + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/list/SubscriptionsAdapter.kt b/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/list/SubscriptionsAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..3c0ec03f2c76c7f7100bfda6c1ada78a2c57e28a --- /dev/null +++ b/app/src/main/java/org/futo/circles/feature/sign_up/subscription_stage/list/SubscriptionsAdapter.kt @@ -0,0 +1,23 @@ +package org.futo.circles.feature.sign_up.subscription_stage.list + +import android.view.ViewGroup +import org.futo.circles.core.list.BaseRvAdapter +import org.futo.circles.model.SubscriptionListItem + +class SubscriptionsAdapter( + private val onItemClicked: (id: String) -> Unit +) : BaseRvAdapter<SubscriptionListItem, SubscriptionViewHolder>( + DefaultIdEntityCallback() +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder = + SubscriptionViewHolder( + parent = parent, + onSubscriptionClicked = { position -> onItemClicked(getItem(position).id) } + ) + + override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) { + holder.bind(getItem(position)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/feature/sign_up/validate_token/ValidateTokenDataSource.kt b/app/src/main/java/org/futo/circles/feature/sign_up/validate_token/ValidateTokenDataSource.kt index 43864d482ee842cffa9944e49f29f1e958809508..861c4bee47717f3520addfed10d80e832b0e4f6a 100644 --- a/app/src/main/java/org/futo/circles/feature/sign_up/validate_token/ValidateTokenDataSource.kt +++ b/app/src/main/java/org/futo/circles/feature/sign_up/validate_token/ValidateTokenDataSource.kt @@ -3,6 +3,7 @@ package org.futo.circles.feature.sign_up.validate_token import org.futo.circles.extensions.Response import org.futo.circles.extensions.createResult import org.futo.circles.feature.sign_up.SignUpDataSource +import org.futo.circles.feature.sign_up.SignUpDataSource.Companion.TYPE_PARAM_KEY import org.futo.circles.provider.MatrixInstanceProvider import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.Stage @@ -21,8 +22,8 @@ class ValidateTokenDataSource( val result = createResult { wizard.registrationCustom( mapOf( - "type" to type, - "token" to token + TYPE_PARAM_KEY to type, + TOKEN_PARAM_KEY to token ) ) } @@ -31,4 +32,8 @@ class ValidateTokenDataSource( return result } + + companion object { + private const val TOKEN_PARAM_KEY = "token" + } } \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/model/SubscriptionListItem.kt b/app/src/main/java/org/futo/circles/model/SubscriptionListItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..fc3f1aa8b889be369dd45fcbd2ad175e04bf377d --- /dev/null +++ b/app/src/main/java/org/futo/circles/model/SubscriptionListItem.kt @@ -0,0 +1,11 @@ +package org.futo.circles.model + +import org.futo.circles.core.list.IdEntity + +data class SubscriptionListItem( + override val id: String, + val name: String, + val description: String, + val price: String, + val duration: String +) : IdEntity<String> \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/subscriptions/IsoPeriod.kt b/app/src/main/java/org/futo/circles/subscriptions/IsoPeriod.kt new file mode 100644 index 0000000000000000000000000000000000000000..d9f9b2fb60d866a631034241bb1ec8e0a2cb84a0 --- /dev/null +++ b/app/src/main/java/org/futo/circles/subscriptions/IsoPeriod.kt @@ -0,0 +1,22 @@ +package org.futo.circles.subscriptions + +import android.content.Context +import org.futo.circles.R + +fun String.formatIsoPeriod(context: Context): String = toDurationNumberPairs() + .joinToString(separator = " ") { (number, duration) -> + context.resources.getQuantityString( + when (duration) { + "D" -> R.plurals.days + "W" -> R.plurals.weeks + "M" -> R.plurals.months + "Y" -> R.plurals.years + + else -> R.plurals.days + }, number, number + ) + } + +private fun String.toDurationNumberPairs() = removePrefix("P") + .chunked(2) + .map { it[0].toString().toInt() to it[1].toString() } \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/subscriptions/ItemPurchasedListener.kt b/app/src/main/java/org/futo/circles/subscriptions/ItemPurchasedListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..ceebad92d08050369c92a3bb168f6c92ed4ab380 --- /dev/null +++ b/app/src/main/java/org/futo/circles/subscriptions/ItemPurchasedListener.kt @@ -0,0 +1,10 @@ +package org.futo.circles.subscriptions + + +interface ItemPurchasedListener { + + fun onItemPurchased(purchase: String) + + fun onPurchaseFailed(errorCode: Int) + +} \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/subscriptions/SubscriptionManager.kt b/app/src/main/java/org/futo/circles/subscriptions/SubscriptionManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..648fb89c546a7bc23f4e3917f45fc7ded101fa07 --- /dev/null +++ b/app/src/main/java/org/futo/circles/subscriptions/SubscriptionManager.kt @@ -0,0 +1,12 @@ +package org.futo.circles.subscriptions + +import org.futo.circles.extensions.Response +import org.futo.circles.model.SubscriptionListItem + +interface SubscriptionManager { + + suspend fun getDetails(productIds: List<String>): Response<List<SubscriptionListItem>> + + suspend fun purchaseProduct(productId: String): Response<Unit> + +} \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/subscriptions/SubscriptionProvider.kt b/app/src/main/java/org/futo/circles/subscriptions/SubscriptionProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..1fe9597c832b6ef02cb47fa8f1a739f2507595b5 --- /dev/null +++ b/app/src/main/java/org/futo/circles/subscriptions/SubscriptionProvider.kt @@ -0,0 +1,12 @@ +package org.futo.circles.subscriptions + +import android.app.Activity + +interface SubscriptionProvider { + + fun getManager( + activity: Activity, + itemPurchaseListener: ItemPurchasedListener + ): SubscriptionManager + +} \ No newline at end of file diff --git a/app/src/main/res/layout/select_sign_up_type_fragment.xml b/app/src/main/res/layout/select_sign_up_type_fragment.xml index 63715b29ed568c875bb6c47af622a87817bbab71..77d3e6de40c9de257f55e48d35fba84414c44445 100644 --- a/app/src/main/res/layout/select_sign_up_type_fragment.xml +++ b/app/src/main/res/layout/select_sign_up_type_fragment.xml @@ -129,7 +129,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:gravity="center" - android:text="@string/already_have_a_circles_token" + android:text="@string/already_have_a_token" app:layout_constraintBottom_toTopOf="@id/btnToken" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -170,44 +170,27 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:gravity="center" - android:text="@string/create_a_circles_subscription" + android:text="@string/create_a_subscription" app:layout_constraintBottom_toTopOf="@id/btnSubscription" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvOr" /> - <com.google.android.material.button.MaterialButton + <org.futo.circles.view.LoadingButton android:id="@+id/btnSubscription" - style="@style/AccentButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:enabled="false" - android:text="@string/new_circles_subscription" - app:layout_constraintBottom_toTopOf="@id/tvSubscriptionUnavailable" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/tvSubscriptionTitle" /> - - <TextView - android:id="@+id/tvSubscriptionUnavailable" - style="@style/footNote" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="8dp" - android:gravity="center" - android:text="@string/new_paid_circles_subscriptions_are_currently_unavailable" - app:drawableStartCompat="@drawable/ic_error" - app:drawableTint="@color/inactive_menu_icon_color" + android:text="@string/choose_a_subscription" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/btnSubscription" /> - + app:layout_constraintTop_toBottomOf="@id/tvSubscriptionTitle" /> <androidx.constraintlayout.widget.Group android:id="@+id/groupSubscription" android:layout_width="0dp" android:layout_height="0dp" - app:constraint_referenced_ids="tvOr,tvSubscriptionTitle,btnSubscription,tvSubscriptionUnavailable" /> + app:constraint_referenced_ids="tvOr,tvSubscriptionTitle,btnSubscription" /> </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/subscription_list_item.xml b/app/src/main/res/layout/subscription_list_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..acb06f4a3a7407e352859aaf3bf0f1c886c9237c --- /dev/null +++ b/app/src/main/res/layout/subscription_list_item.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:padding="4dp"> + + + <com.google.android.material.imageview.ShapeableImageView + android:id="@+id/ivIcon" + android:layout_width="@dimen/group_icon_size" + android:layout_height="@dimen/group_icon_size" + android:scaleType="centerCrop" + android:src="@mipmap/ic_launcher" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.GroupIconRadius" /> + + + <TextView + android:id="@+id/tvName" + style="@style/title2" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:ellipsize="end" + android:lines="1" + app:layout_constraintBottom_toTopOf="@id/tvPrice" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/ivIcon" + app:layout_constraintTop_toTopOf="@id/ivIcon" + tools:text="texsdt" /> + + + <TextView + android:id="@+id/tvPrice" + style="@style/subheadline" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:ellipsize="end" + android:lines="1" + app:layout_constraintBottom_toTopOf="@id/tvDuration" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/tvName" + app:layout_constraintTop_toBottomOf="@id/tvName" + tools:text="texsdt" /> + + <TextView + android:id="@+id/tvDuration" + style="@style/subheadline" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:ellipsize="end" + android:lines="1" + app:layout_constraintBottom_toTopOf="@id/tvDetails" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/tvName" + app:layout_constraintTop_toBottomOf="@id/tvPrice" + tools:text="texsdt" /> + + <TextView + android:id="@+id/tvDetails" + style="@style/subheadline" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:ellipsize="end" + android:lines="1" + app:layout_constraintBottom_toBottomOf="@id/ivIcon" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/tvName" + app:layout_constraintTop_toBottomOf="@id/tvDuration" + tools:text="texsdt" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/subscription_stage_fragment.xml b/app/src/main/res/layout/subscription_stage_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..b8bbd8e9665850a4647798aa0b33e3238d62d570 --- /dev/null +++ b/app/src/main/res/layout/subscription_stage_fragment.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guidelineLogo" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent="0.35" /> + + <org.futo.circles.view.CirclesLogoView + android:id="@+id/ivLogo" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginHorizontal="35dp" + android:layout_marginTop="24dp" + app:layout_constraintBottom_toTopOf="@id/guidelineLogo" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/tvSubscriptionTitle" + style="@style/title2" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:gravity="center" + android:text="@string/subscriptions" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/guidelineLogo" + app:layout_constraintVertical_bias="0.3" + app:layout_constraintVertical_chainStyle="packed" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/rvSubscriptions" + android:layout_width="0dp" + android:layout_height="0dp" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/tvSubscriptionTitle" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/navigation/sign_up_nav_graph.xml b/app/src/main/res/navigation/sign_up_nav_graph.xml index 872fd83f131647257234fd492b96b47a0f010635..9f1cd8081aa68b0be2b93206c632334cfd4d1b85 100644 --- a/app/src/main/res/navigation/sign_up_nav_graph.xml +++ b/app/src/main/res/navigation/sign_up_nav_graph.xml @@ -17,29 +17,34 @@ android:id="@+id/to_acceptTerms" app:destination="@id/acceptTermsFragment" /> + <action + android:id="@+id/to_subscriptions" + app:destination="@id/subscriptionStageFragment" /> + <fragment android:id="@+id/selectSignUpTypeFragment" android:name="org.futo.circles.feature.sign_up.sign_up_type.SelectSignUpTypeFragment" - android:label="SelectSignUpTypeFragment" tools:layout="@layout/select_sign_up_type_fragment" /> <fragment android:id="@+id/validateTokenFragment" android:name="org.futo.circles.feature.sign_up.validate_token.ValidateTokenFragment" - android:label="ValidateTokenFragment" tools:layout="@layout/validate_token_fragment" /> <fragment android:id="@+id/validateEmailFragment" android:name="org.futo.circles.feature.sign_up.validate_email.ValidateEmailFragment" - android:label="CreateAccountFragment" tools:layout="@layout/validate_email_fragment" /> <fragment android:id="@+id/acceptTermsFragment" android:name="org.futo.circles.feature.sign_up.terms.AcceptTermsFragment" - android:label="AcceptTermsFragment" tools:layout="@layout/accept_terms_fragment" /> + <fragment + android:id="@+id/subscriptionStageFragment" + android:name="org.futo.circles.feature.sign_up.subscription_stage.SubscriptionStageFragment" + tools:layout="@layout/subscription_stage_fragment" /> + </navigation> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bf2a97bca53551c8ed29f632b134e1db5e35977a..b1d65db39d8c7660650621947a0a88438628c49f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,11 +57,10 @@ <string name="group_updated">Group updated</string> <string name="circle_updated">Circle updated</string> <string name="gallery_updated">Gallery updated</string> - <string name="already_have_a_circles_token">Already have a Circles token?</string> + <string name="already_have_a_token">Already have a token?</string> <string name="sign_up_with_token">Sign up with token</string> - <string name="new_circles_subscription">New Circles subscription</string> - <string name="new_paid_circles_subscriptions_are_currently_unavailable">New paid Circles subscriptions are currently unavailable</string> - <string name="create_a_circles_subscription">Create a Circles subscription</string> + <string name="choose_a_subscription">Choose a subscription</string> + <string name="create_a_subscription">Create a subscription</string> <string name="or">Or</string> <string name="validate_your_token">Validate your token</string> <string name="validate_token">Validate token</string> @@ -238,6 +237,17 @@ <string name="enable_cross_signing">Enable Cross Signing</string> <string name="enable_cross_signing_message">Confirm auth to enable cross signing</string> <string name="device_media">Device media</string> + <string name="subscriptions">Subscriptions</string> + <string name="wrong_signup_config">Wrong signup config!</string> + <string name="item_already_owned">Item already owned</string> + <string name="purchase_failed_format">Purchase failed with code %d</string> + <string name="feature_not_supported">Feature is not supported</string> + <string name="service_unavailable">Service unavailable</string> + <string name="item_unavailable">Item is unavailable</string> + <string name="user_canceled">User canceled</string> + <string name="item_not_owned">Item is not owned</string> + <string name="developer_error">Developer error</string> + <string name="flavour_does_not_support_subscriptions">This flavour does not support subscriptions</string> <string-array name="report_categories"> <item>@string/crude_language</item> @@ -268,4 +278,30 @@ <item quantity="one">Show %d reply</item> <item quantity="other">Show %d replies</item> </plurals> + + + <plurals name="days"> + <item quantity="one">%1$d day</item> + <item quantity="many">%1$d days</item> + <item quantity="other">%1$d days</item> + </plurals> + + <plurals name="weeks"> + <item quantity="one">%1$d week</item> + <item quantity="many">%1$d weeks</item> + <item quantity="other">%1$d weeks</item> + </plurals> + + <plurals name="months"> + <item quantity="one">%1$d month</item> + <item quantity="many">%1$d months</item> + <item quantity="other">%1$d months</item> + </plurals> + + <plurals name="years"> + <item quantity="one">%1$d year</item> + <item quantity="many">%1$d years</item> + <item quantity="other">%1$d years</item> + </plurals> + </resources> \ No newline at end of file diff --git a/build.gradle b/build.gradle index f225addfa13a29495b5940f048be1626fad4ddb8..ef170973ee5403feec32de90c6a7838a4a65ff3b 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { kotlin_version = '1.7.0' minSdkVersion = 24 sdkVersion = 32 - nav_version = "2.5.0" + androidx_version = "2.5.1" } repositories { google() @@ -13,7 +13,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$androidx_version" classpath 'com.google.gms:google-services:4.3.13' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1' }