Compare commits

...

5 Commits

Author SHA1 Message Date
Hans-Christoph Steiner 3eaa30d7aa
Merge b04c52a486 into 317c12fdd4 2024-04-30 01:22:26 +02:00
Mateusz Armatys 317c12fdd4 fix(plan): Update UI of `UpgradeStorageInfo` to avoid truncated text. 2024-04-29 17:11:09 +00:00
Neil Marietta 64c3b4de6f fix(plan): Fixed Subscription Update error handling (e.g. network issue). 2024-04-29 16:32:30 +00:00
Neil Marietta 2c4b8e9aba feat(auth): Added Cancel Create Account Dialog. 2024-04-29 17:15:45 +02:00
Hans-Christoph Steiner b04c52a486 improve detection of GitLab CI
The `CI` env var is set by all CI systems, including GitLab CI, GitHub
Actions, Travis CI, Circle CI, etc.  This code clearly is looking to
detect GitLab CI, since the code that is run when `isCI` is true
requires settings from env vars specific to GitLab CI.
2024-01-04 12:34:19 +01:00
23 changed files with 357 additions and 92 deletions

View File

@ -2,6 +2,10 @@ public class hilt_aggregated_deps/_me_proton_core_auth_presentation_MissingScope
public fun <init> ()V
}
public class hilt_aggregated_deps/_me_proton_core_auth_presentation_alert_CancelCreateAccountDialog_GeneratedInjector {
public fun <init> ()V
}
public class hilt_aggregated_deps/_me_proton_core_auth_presentation_alert_confirmpass_ConfirmPasswordDialog_GeneratedInjector {
public fun <init> ()V
}
@ -94,6 +98,14 @@ public class hilt_aggregated_deps/_me_proton_core_auth_presentation_viewmodel_Ad
public fun <init> ()V
}
public class hilt_aggregated_deps/_me_proton_core_auth_presentation_viewmodel_CancelCreateAccountDialogViewModel_HiltModules_BindsModule {
public fun <init> ()V
}
public class hilt_aggregated_deps/_me_proton_core_auth_presentation_viewmodel_CancelCreateAccountDialogViewModel_HiltModules_KeyModule {
public fun <init> ()V
}
public class hilt_aggregated_deps/_me_proton_core_auth_presentation_viewmodel_ChooseAddressViewModel_HiltModules_BindsModule {
public fun <init> ()V
}
@ -342,6 +354,29 @@ public final class me/proton/core/auth/presentation/alert/AlertUtilsKt {
public static synthetic fun showPasswordEnterDialog$default (Landroidx/fragment/app/FragmentManager;ZZZILjava/lang/Object;)V
}
public final class me/proton/core/auth/presentation/alert/CancelCreateAccountDialog : androidx/fragment/app/DialogFragment {
public fun <init> ()V
public fun onCreateDialog (Landroid/os/Bundle;)Landroid/app/Dialog;
public final fun show (Landroidx/fragment/app/FragmentManager;Lkotlin/jvm/functions/Function0;)V
}
public abstract interface class me/proton/core/auth/presentation/alert/CancelCreateAccountDialog_GeneratedInjector {
public abstract fun injectCancelCreateAccountDialog (Lme/proton/core/auth/presentation/alert/CancelCreateAccountDialog;)V
}
public abstract class me/proton/core/auth/presentation/alert/Hilt_CancelCreateAccountDialog : androidx/fragment/app/DialogFragment, dagger/hilt/internal/GeneratedComponentManagerHolder {
public final fun componentManager ()Ldagger/hilt/android/internal/managers/FragmentComponentManager;
public synthetic fun componentManager ()Ldagger/hilt/internal/GeneratedComponentManager;
protected fun createComponentManager ()Ldagger/hilt/android/internal/managers/FragmentComponentManager;
public final fun generatedComponent ()Ljava/lang/Object;
public fun getContext ()Landroid/content/Context;
public fun getDefaultViewModelProviderFactory ()Landroidx/lifecycle/ViewModelProvider$Factory;
protected fun inject ()V
public fun onAttach (Landroid/app/Activity;)V
public fun onAttach (Landroid/content/Context;)V
public fun onGetLayoutInflater (Landroid/os/Bundle;)Landroid/view/LayoutInflater;
}
public final class me/proton/core/auth/presentation/alert/PasswordAnd2FADialog : androidx/fragment/app/DialogFragment {
public static final field BUNDLE_KEY_PASS_2FA_DATA Ljava/lang/String;
public static final field Companion Lme/proton/core/auth/presentation/alert/PasswordAnd2FADialog$Companion;
@ -554,6 +589,16 @@ public final class me/proton/core/auth/presentation/databinding/ContentHelpItemO
public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Z)Lme/proton/core/auth/presentation/databinding/ContentHelpItemOtherIssuesBinding;
}
public final class me/proton/core/auth/presentation/databinding/DialogCancelCreationBinding : androidx/viewbinding/ViewBinding {
public final field cancelButton Lme/proton/core/presentation/ui/view/ProtonButton;
public final field continueButton Lme/proton/core/presentation/ui/view/ProtonButton;
public static fun bind (Landroid/view/View;)Lme/proton/core/auth/presentation/databinding/DialogCancelCreationBinding;
public synthetic fun getRoot ()Landroid/view/View;
public fun getRoot ()Landroidx/constraintlayout/widget/ConstraintLayout;
public static fun inflate (Landroid/view/LayoutInflater;)Lme/proton/core/auth/presentation/databinding/DialogCancelCreationBinding;
public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Z)Lme/proton/core/auth/presentation/databinding/DialogCancelCreationBinding;
}
public final class me/proton/core/auth/presentation/databinding/DialogConfirmPasswordBinding : androidx/viewbinding/ViewBinding {
public final field cancelButton Lme/proton/core/presentation/ui/view/ProtonButton;
public final field enterButton Lme/proton/core/presentation/ui/view/ProtonProgressButton;
@ -2140,6 +2185,38 @@ public final class me/proton/core/auth/presentation/viewmodel/AddAccountViewMode
public static fun provide ()Ljava/lang/String;
}
public final class me/proton/core/auth/presentation/viewmodel/CancelCreateAccountDialogViewModel : me/proton/core/presentation/viewmodel/ProtonViewModel {
public fun <init> (Lme/proton/core/accountmanager/domain/AccountWorkflowHandler;Lme/proton/core/accountmanager/domain/AccountManager;Lme/proton/core/util/kotlin/CoroutineScopeProvider;)V
public final fun cancelCreation ()Lkotlinx/coroutines/Job;
}
public final class me/proton/core/auth/presentation/viewmodel/CancelCreateAccountDialogViewModel_Factory : dagger/internal/Factory {
public fun <init> (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V
public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/auth/presentation/viewmodel/CancelCreateAccountDialogViewModel_Factory;
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Lme/proton/core/auth/presentation/viewmodel/CancelCreateAccountDialogViewModel;
public static fun newInstance (Lme/proton/core/accountmanager/domain/AccountWorkflowHandler;Lme/proton/core/accountmanager/domain/AccountManager;Lme/proton/core/util/kotlin/CoroutineScopeProvider;)Lme/proton/core/auth/presentation/viewmodel/CancelCreateAccountDialogViewModel;
}
public final class me/proton/core/auth/presentation/viewmodel/CancelCreateAccountDialogViewModel_HiltModules {
}
public abstract class me/proton/core/auth/presentation/viewmodel/CancelCreateAccountDialogViewModel_HiltModules$BindsModule {
public abstract fun binds (Lme/proton/core/auth/presentation/viewmodel/CancelCreateAccountDialogViewModel;)Landroidx/lifecycle/ViewModel;
}
public final class me/proton/core/auth/presentation/viewmodel/CancelCreateAccountDialogViewModel_HiltModules$KeyModule {
public static fun provide ()Ljava/lang/String;
}
public final class me/proton/core/auth/presentation/viewmodel/CancelCreateAccountDialogViewModel_HiltModules_KeyModule_ProvideFactory : dagger/internal/Factory {
public fun <init> ()V
public static fun create ()Lme/proton/core/auth/presentation/viewmodel/CancelCreateAccountDialogViewModel_HiltModules_KeyModule_ProvideFactory;
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Ljava/lang/String;
public static fun provide ()Ljava/lang/String;
}
public final class me/proton/core/auth/presentation/viewmodel/ChooseAddressViewModel : me/proton/core/presentation/viewmodel/ProtonViewModel, me/proton/core/observability/domain/ObservabilityContext {
public fun <init> (Lme/proton/core/accountmanager/domain/AccountWorkflowHandler;Lme/proton/core/auth/domain/usecase/AccountAvailability;Lme/proton/core/observability/domain/ObservabilityManager;Lme/proton/core/auth/domain/usecase/PostLoginAccountSetup;Lme/proton/core/usersettings/domain/usecase/SetupUsername;)V
public fun enqueueObservability (Lme/proton/core/observability/domain/metrics/ObservabilityData;)V

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2024 Proton AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.auth.presentation.alert
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import me.proton.core.auth.presentation.R
import me.proton.core.auth.presentation.databinding.DialogCancelCreationBinding
import me.proton.core.auth.presentation.viewmodel.CancelCreateAccountDialogViewModel
import me.proton.core.presentation.utils.onClick
@AndroidEntryPoint
class CancelCreateAccountDialog : DialogFragment() {
private val viewModel by viewModels<CancelCreateAccountDialogViewModel>()
private var onCreationCancelled: (() -> Unit)? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
super.onCreateDialog(savedInstanceState)
val binding = DialogCancelCreationBinding.inflate(LayoutInflater.from(requireContext()))
// Buttons are in a custom View to avoid dismissing dialog on click (cancel scope).
binding.continueButton.onClick {
dismiss()
}
binding.cancelButton.onClick {
viewModel.cancelCreation().invokeOnCompletion {
dismiss()
onCreationCancelled?.invoke()
}
}
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.auth_signup_create_account_dialog_title)
.setMessage(R.string.auth_signup_create_account_dialog_text)
.setView(binding.root)
.create()
}
fun show(fragmentManager: FragmentManager, onCreationCancelled: () -> Unit) {
this.onCreationCancelled = onCreationCancelled
show(fragmentManager, null)
}
}

View File

@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import me.proton.core.account.domain.entity.AccountType
import me.proton.core.auth.presentation.R
import me.proton.core.auth.presentation.alert.CancelCreateAccountDialog
import me.proton.core.auth.presentation.databinding.FragmentSignupChooseExternalEmailBinding
import me.proton.core.auth.presentation.entity.signup.SubscriptionDetails
import me.proton.core.auth.presentation.ui.onLongState
@ -96,7 +97,10 @@ class ChooseExternalEmailFragment : SignupFragment(R.layout.fragment_signup_choo
signupViewModel.onFinish()
activity?.finish()
} else {
showError(getString(R.string.auth_signup_error_create_to_continue))
CancelCreateAccountDialog().show(parentFragmentManager) {
signupViewModel.onFinish()
activity?.finish()
}
}
}
@ -104,7 +108,7 @@ class ChooseExternalEmailFragment : SignupFragment(R.layout.fragment_signup_choo
super.onViewCreated(view, savedInstanceState)
binding.apply {
toolbar.setNavigationIcon(R.drawable.ic_proton_close, cancellable)
toolbar.setNavigationIcon(R.drawable.ic_proton_close, true)
toolbar.setNavigationOnClickListener { onBackPressed() }
emailInput.text = email

View File

@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import me.proton.core.account.domain.entity.AccountType
import me.proton.core.auth.presentation.R
import me.proton.core.auth.presentation.alert.CancelCreateAccountDialog
import me.proton.core.auth.presentation.databinding.FragmentSignupChooseInternalEmailBinding
import me.proton.core.auth.presentation.ui.onLongState
import me.proton.core.auth.presentation.viewmodel.signup.ChooseInternalEmailViewModel
@ -90,7 +91,10 @@ class ChooseInternalEmailFragment : SignupFragment(R.layout.fragment_signup_choo
signupViewModel.onFinish()
activity?.finish()
} else {
showError(getString(R.string.auth_signup_error_create_to_continue))
CancelCreateAccountDialog().show(parentFragmentManager) {
signupViewModel.onFinish()
activity?.finish()
}
}
}
@ -98,7 +102,7 @@ class ChooseInternalEmailFragment : SignupFragment(R.layout.fragment_signup_choo
super.onViewCreated(view, savedInstanceState)
binding.apply {
toolbar.setNavigationIcon(R.drawable.ic_proton_close, cancellable)
toolbar.setNavigationIcon(R.drawable.ic_proton_close, true)
toolbar.setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() }
usernameInput.apply {

View File

@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import me.proton.core.account.domain.entity.AccountType
import me.proton.core.auth.presentation.R
import me.proton.core.auth.presentation.alert.CancelCreateAccountDialog
import me.proton.core.auth.presentation.databinding.FragmentSignupChooseUsernameBinding
import me.proton.core.auth.presentation.ui.onLongState
import me.proton.core.auth.presentation.viewmodel.signup.ChooseUsernameViewModel
@ -63,7 +64,10 @@ class ChooseUsernameFragment : SignupFragment(R.layout.fragment_signup_choose_us
signupViewModel.onFinish()
activity?.finish()
} else {
showError(getString(R.string.auth_signup_error_create_to_continue))
CancelCreateAccountDialog().show(parentFragmentManager) {
signupViewModel.onFinish()
activity?.finish()
}
}
}
@ -71,7 +75,7 @@ class ChooseUsernameFragment : SignupFragment(R.layout.fragment_signup_choose_us
super.onViewCreated(view, savedInstanceState)
binding.apply {
toolbar.setNavigationIcon(R.drawable.ic_proton_close, cancellable)
toolbar.setNavigationIcon(R.drawable.ic_proton_close, true)
toolbar.setNavigationOnClickListener { onBackPressed() }
usernameInput.apply {

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2024 Proton AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.auth.presentation.viewmodel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.accountmanager.domain.AccountWorkflowHandler
import me.proton.core.presentation.viewmodel.ProtonViewModel
import me.proton.core.util.kotlin.CoroutineScopeProvider
import javax.inject.Inject
@HiltViewModel
class CancelCreateAccountDialogViewModel @Inject constructor(
private val accountWorkflowHandler: AccountWorkflowHandler,
private val accountManager: AccountManager,
private val scopeProvider: CoroutineScopeProvider
) : ProtonViewModel() {
fun cancelCreation() = scopeProvider.GlobalDefaultSupervisedScope.launch {
accountManager.getPrimaryUserId().firstOrNull()?.let {
accountWorkflowHandler.handleCreateAccountFailed(it)
accountManager.disableAccount(it, keepSession = true)
}
}
}

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2024 Proton AG
~ This file is part of Proton AG and ProtonCore.
~
~ ProtonCore is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ ProtonCore is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
-->
<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="wrap_content"
android:padding="@dimen/default_margin">
<me.proton.core.presentation.ui.view.ProtonButton
android:id="@+id/continueButton"
style="@style/ProtonButton.Borderless.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/auth_signup_create_account_dialog_action_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<me.proton.core.presentation.ui.view.ProtonButton
android:id="@+id/cancelButton"
style="@style/ProtonButton.Borderless.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/auth_signup_create_account_dialog_action_cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/continueButton"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -132,6 +132,11 @@
<string name="auth_signup_set_recovery">Set recovery method</string>
<string name="auth_signup_no_connectivity">No connectivity to display Terms and Conditions.</string>
<string name="auth_signup_error_create_to_continue">Please create an account to continue</string>
<string name="auth_signup_error_create_to_continue_action_learn_more">Learn more</string>
<string name="auth_signup_create_account_dialog_title">Activate your subscription</string>
<string name="auth_signup_create_account_dialog_text">Your Google Pay payment was successfully processed. To activate your subscription, please create your Proton account.</string>
<string name="auth_signup_create_account_dialog_action_cancel">Cancel</string>
<string name="auth_signup_create_account_dialog_action_continue">Continue</string>
<string name="auth_signup_error_username_blank">Username must not be blank.</string>
<string name="auth_signup_error_passwords_do_not_match">Passwords do not match.</string>
<string name="auth_signup_validation_password_length">Password should be at least 8 characters long.</string>

View File

@ -175,11 +175,14 @@ public final class me/proton/core/network/domain/ApiResultKt {
public static final fun hasProtonErrorCode (Lme/proton/core/network/domain/ApiException;I)Z
public static final fun isForceUpdate (Lme/proton/core/network/domain/ApiException;)Z
public static final fun isForceUpdate (Lme/proton/core/network/domain/ApiResult;)Z
public static final fun isHttpError (Lme/proton/core/network/domain/ApiException;I)Z
public static final fun isHttpError (Lme/proton/core/network/domain/ApiResult;I)Z
public static final fun isPotentialBlocking (Ljava/lang/Throwable;)Z
public static final fun isRetryable (Lme/proton/core/network/domain/ApiException;)Z
public static final fun isRetryable (Lme/proton/core/network/domain/ApiResult;)Z
public static final fun isUnauthorized (Lme/proton/core/network/domain/ApiException;)Z
public static final fun isUnauthorized (Lme/proton/core/network/domain/ApiResult;)Z
public static final fun isUnprocessable (Lme/proton/core/network/domain/ApiException;)Z
public static final fun onError (Lme/proton/core/network/domain/ApiResult;Lkotlin/jvm/functions/Function1;)Lme/proton/core/network/domain/ApiResult;
public static final fun onParseError (Lme/proton/core/network/domain/ApiResult;Lkotlin/jvm/functions/Function1;)Lme/proton/core/network/domain/ApiResult;
public static final fun onParseErrorLog (Lme/proton/core/network/domain/ApiResult;Ljava/lang/String;)Lme/proton/core/network/domain/ApiResult;

View File

@ -167,19 +167,34 @@ fun ApiException.hasProtonErrorCode(code: Int): Boolean =
(error as? ApiResult.Error.Http)?.proton?.code == code
/**
* Return true if [ApiException.error] is an unauthorized error (401).
* Return true if [ApiException.error] is a http error equals [code].
*
* @see ApiResult.isUnauthorized
* @see ApiResult.isHttpError
*/
fun ApiException.isUnauthorized(): Boolean = error.isUnauthorized()
fun ApiException.isHttpError(code: Int): Boolean = error.isHttpError(code)
/**
* Return true if [ApiResult] is a http error equals [code].
*/
fun <T> ApiResult<T>.isHttpError(code: Int): Boolean {
val httpError = this as? ApiResult.Error.Http
return httpError?.httpCode == code
}
/**
* Return true if [ApiResult] is an unauthorized error (401).
*/
fun <T> ApiResult<T>.isUnauthorized(): Boolean {
val httpError = this as? ApiResult.Error.Http
return httpError?.httpCode == HttpResponseCodes.HTTP_UNAUTHORIZED
}
fun <T> ApiResult<T>.isUnauthorized(): Boolean = isHttpError(HttpResponseCodes.HTTP_UNAUTHORIZED)
/**
* Return true if [ApiException.error] is an unauthorized error (401).
*/
fun ApiException.isUnauthorized(): Boolean = isHttpError(HttpResponseCodes.HTTP_UNAUTHORIZED)
/**
* Return true if [ApiException.error] is an unprocessable error (422).
*/
fun ApiException.isUnprocessable() = isHttpError(HttpResponseCodes.HTTP_UNPROCESSABLE)
/**
* Return true if [ApiException.error] is a force update error (5003/5005).

View File

@ -29,7 +29,7 @@ protonBuild {
}
protonCoverage {
branchCoveragePercentage.set(51)
branchCoveragePercentage.set(48)
lineCoveragePercentage.set(59)
}

View File

@ -31,11 +31,13 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import me.proton.core.network.domain.ApiException
import me.proton.core.network.domain.isRetryable
import me.proton.core.network.domain.isUnprocessable
import me.proton.core.network.domain.session.SessionProvider
import me.proton.core.observability.domain.ObservabilityContext
import me.proton.core.observability.domain.ObservabilityManager
import me.proton.core.payment.domain.MAX_PLAN_QUANTITY
import me.proton.core.payment.domain.entity.PaymentTokenEntity
import me.proton.core.payment.domain.entity.Purchase
import me.proton.core.payment.domain.entity.PurchaseState
import me.proton.core.payment.domain.entity.SubscriptionCycle
import me.proton.core.payment.domain.extension.getSubscribeObservabilityData
@ -78,25 +80,42 @@ internal class SubscribePurchaseWorker @AssistedInject constructor(
subscriptionManagement = SubscriptionManagement.GOOGLE_MANAGED
)
}.fold(
onSuccess = {
purchaseRepository.upsertPurchase(purchase.copy(purchaseState = PurchaseState.Subscribed))
Result.success()
},
onFailure = {
if (it is ApiException && it.isRetryable()) {
Result.retry()
} else {
CoreLogger.e(TAG, it)
purchaseRepository.upsertPurchase(
purchase.copy(
purchaseFailure = it.localizedMessage,
purchaseState = PurchaseState.Failed
)
)
Result.failure()
onSuccess = { onSuccess(purchase) },
onFailure = { onFailure(purchase, it) }
)
}
private suspend fun onSuccess(purchase: Purchase): Result {
purchaseRepository.upsertPurchase(purchase.copy(purchaseState = PurchaseState.Subscribed))
return Result.success()
}
private suspend fun onFailure(purchase: Purchase, error: Throwable): Result {
return when {
error is ApiException && error.isRetryable() -> Result.retry()
error is ApiException && error.isUnprocessable() -> {
val userId = requireNotNull(sessionProvider.getUserId(purchase.sessionId))
val plans = plansRepository.getSubscription(userId)?.plans.orEmpty()
when {
plans.any { it.name == purchase.planName } -> onSuccess(purchase)
else -> onPermanentFailure(purchase, error)
}
}
else -> onPermanentFailure(purchase, error)
}
}
private suspend fun onPermanentFailure(purchase: Purchase, error: Throwable): Result {
CoreLogger.e(TAG, error)
purchaseRepository.upsertPurchase(
purchase.copy(
purchaseFailure = error.localizedMessage,
purchaseState = PurchaseState.Failed
)
)
return Result.failure()
}
companion object {

View File

@ -19,13 +19,11 @@ public final class me/proton/core/plan/presentation/compose/component/Composable
public static field lambda-2 Lkotlin/jvm/functions/Function2;
public static field lambda-3 Lkotlin/jvm/functions/Function2;
public static field lambda-4 Lkotlin/jvm/functions/Function2;
public static field lambda-5 Lkotlin/jvm/functions/Function2;
public fun <init> ()V
public final fun getLambda-1$plan_presentation_compose_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-2$plan_presentation_compose_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-3$plan_presentation_compose_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-4$plan_presentation_compose_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-5$plan_presentation_compose_release ()Lkotlin/jvm/functions/Function2;
}
public final class me/proton/core/plan/presentation/compose/component/UpgradeStorageInfoKt {

View File

@ -27,7 +27,7 @@ plugins {
publishOption.shouldBePublishedAsLib = true
protonCoverage {
branchCoveragePercentage.set(50)
branchCoveragePercentage.set(51)
lineCoveragePercentage.set(91)
}

View File

@ -19,6 +19,7 @@
package me.proton.core.plan.presentation.compose.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -26,8 +27,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -38,9 +38,7 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import me.proton.core.compose.component.ProtonSolidButton
import me.proton.core.compose.component.protonButtonColors
import me.proton.core.compose.flow.rememberAsState
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import me.proton.core.compose.theme.ProtonColors
import me.proton.core.compose.theme.ProtonDimens.DefaultIconSize
import me.proton.core.compose.theme.ProtonDimens.ExtraSmallIconSize
@ -52,7 +50,6 @@ import me.proton.core.domain.entity.UserId
import me.proton.core.plan.presentation.R
import me.proton.core.plan.presentation.compose.viewmodel.AccountStorageState
import me.proton.core.plan.presentation.compose.viewmodel.UpgradeStorageInfoViewModel
import me.proton.core.plan.presentation.compose.viewmodel.UpgradeStorageInfoViewModel.Companion.INITIAL_STATE
/** Displays information that storage is (nearly) full.
* Only applies to users with free plan.
@ -68,13 +65,13 @@ public fun UpgradeStorageInfo(
withBottomDivider: Boolean = false,
) {
if (viewModel == null) return
val state by rememberAsState(flow = viewModel.state, initial = INITIAL_STATE)
val state by viewModel.state.collectAsStateWithLifecycle()
val userId = (state as? AccountStorageState.HighStorageUsage)?.userId
if (state !is AccountStorageState.Hidden) {
Column {
if (withTopDivider) {
Divider(color = ProtonTheme.colors.separatorNorm)
HorizontalDivider(color = ProtonTheme.colors.separatorNorm)
}
UpgradeStorageInfo(
onUpgradeClicked = if (userId != null) {
@ -86,7 +83,7 @@ public fun UpgradeStorageInfo(
modifier = modifier
)
if (withBottomDivider) {
Divider(color = ProtonTheme.colors.separatorNorm)
HorizontalDivider(color = ProtonTheme.colors.separatorNorm)
}
}
}
@ -99,17 +96,23 @@ public fun UpgradeStorageInfo(
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.padding(
horizontal = dimensionResource(id = R.dimen.gap_large),
vertical = dimensionResource(id = R.dimen.gap_medium_plus)
),
modifier = modifier
.clickable(onClick = onUpgradeClicked)
.padding(
horizontal = dimensionResource(id = R.dimen.gap_large),
vertical = dimensionResource(id = R.dimen.gap_medium_plus)
),
verticalAlignment = Alignment.CenterVertically
) {
StackedIcons()
Spacer(modifier = Modifier.width(dimensionResource(id = R.dimen.gap_medium_plus)))
StorageInfoText(title)
Spacer(modifier = Modifier.weight(1.0f))
UpgradeButton(onClick = { onUpgradeClicked() })
StorageInfoText(title, modifier = Modifier.weight(1.0f))
Image(
colorFilter = ColorFilter.tint(ProtonTheme.colors.iconNorm),
contentDescription = null,
modifier = Modifier.size(DefaultIconSize),
painter = painterResource(id = R.drawable.ic_proton_arrow_up),
)
}
}
@ -154,25 +157,6 @@ private fun StorageInfoText(
}
}
@Composable
private fun UpgradeButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
ProtonSolidButton(
colors = ButtonDefaults.protonButtonColors(
backgroundColor = ProtonTheme.colors.interactionWeakNorm
),
onClick = onClick,
modifier = modifier
) {
Text(
color = ProtonTheme.colors.textNorm,
text = stringResource(id = R.string.upgrade_storage_cta_button)
)
}
}
@Composable
private fun AccountStorageState.getTitle(): String = when (this) {
is AccountStorageState.HighStorageUsage.Drive -> stringResource(
@ -204,7 +188,7 @@ internal fun PreviewHighDriveUsage() {
ProtonTheme(colors = ProtonColors.Light.sidebarColors!!) {
UpgradeStorageInfo(
onUpgradeClicked = {},
title = "Drive storage: 90% full"
title = "Drive: 90% full"
)
}
}
@ -215,7 +199,7 @@ internal fun PreviewHighMailUsage() {
ProtonTheme(colors = ProtonColors.Light.sidebarColors!!) {
UpgradeStorageInfo(
onUpgradeClicked = {},
title = "Mail storage: 100% full"
title = "Mail: 100% full"
)
}
}
@ -226,7 +210,7 @@ internal fun PreviewHighMailUsageDarkTheme() {
ProtonTheme(colors = ProtonColors.Dark.sidebarColors!!) {
UpgradeStorageInfo(
onUpgradeClicked = {},
title = "Mail storage: 100% full"
title = "Mail: 100% full"
)
}
}

View File

@ -38,7 +38,7 @@ class UpgradeStorageInfoTest {
WithSidebarColors {
UpgradeStorageInfo(
onUpgradeClicked = {},
title = "Drive storage: 80% full"
title = "Drive: 80% full"
)
}
}
@ -50,7 +50,7 @@ class UpgradeStorageInfoTest {
WithSidebarColors(isDark = true) {
UpgradeStorageInfo(
onUpgradeClicked = {},
title = "Drive storage: 80% full"
title = "Drive: 80% full"
)
}
}
@ -62,7 +62,7 @@ class UpgradeStorageInfoTest {
WithSidebarColors {
UpgradeStorageInfo(
onUpgradeClicked = {},
title = "Mail storage: 100% full"
title = "Mail: 100% full"
)
}
}
@ -74,7 +74,7 @@ class UpgradeStorageInfoTest {
WithSidebarColors(isDark = true) {
UpgradeStorageInfo(
onUpgradeClicked = {},
title = "Mail storage: 100% full"
title = "Mail: 100% full"
)
}
}

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0395aa0ee772298ea3e04fed3180e54d42929ec0f76f370150db2870baa3ecb1
size 14933
oid sha256:f2f522d12d318f85ed25baeafa6dcd746a86b4e2f406b30f9942675681d97012
size 11211

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0395aa0ee772298ea3e04fed3180e54d42929ec0f76f370150db2870baa3ecb1
size 14933
oid sha256:f2f522d12d318f85ed25baeafa6dcd746a86b4e2f406b30f9942675681d97012
size 11211

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2c71c36bdd252036a576e85efc7b97d11ea427fc3e64013de29609bb9cf1d6d6
size 14841
oid sha256:71d35c1cdfeafaab1b69064af776670d64cc6983e9520aadb2366293965973f7
size 11233

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2c71c36bdd252036a576e85efc7b97d11ea427fc3e64013de29609bb9cf1d6d6
size 14841
oid sha256:71d35c1cdfeafaab1b69064af776670d64cc6983e9520aadb2366293965973f7
size 11233

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:804e9c8ef58523c47f0ffd126f3a34d5b67763deac6705013c1ea6a1b513655c
size 14843
oid sha256:d49602dfec3587fa3eea68c36a21ea0bb2c65a17d9bf17aef87de3dd5eb8e453
size 10998

View File

@ -165,9 +165,8 @@
<!-- endregion -->
<!-- region Plan entitlement tags -->
<string name="upgrade_storage_current_drive_storage">Drive storage: %d%% full</string>
<string name="upgrade_storage_current_mail_storage">Mail storage: %d%% full</string>
<string name="upgrade_storage_subtitle">Get more storage</string>
<string name="upgrade_storage_cta_button">Upgrade</string>
<string name="upgrade_storage_current_drive_storage">Drive: %d%% full</string>
<string name="upgrade_storage_current_mail_storage">Mail: %d%% full</string>
<string name="upgrade_storage_subtitle">Upgrade storage</string>
<!-- endregion -->
</resources>

View File

@ -29,17 +29,17 @@ abstract class ProtonIncludeCoreBuildPlugin : Plugin<Settings> {
override fun apply(target: Settings) {
// Configuration from Client.
val config = target.createExtension() as DefaultProtonIncludeCoreBuildExtension
// Configuration from CI.
val isCI = System.getenv("CI").toBoolean()
// Configuration from GitLab CI.
val host = System.getenv("CI_SERVER_HOST")
val token = System.getenv("CI_JOB_TOKEN")
val hasHostToken = host != null && token != null
val commitSha = System.getenv("CORE_COMMIT_SHA")
val parentPath = target.rootDir.parentFile.absolutePath
val metaProperties = File("$parentPath/${metaProperties}")
target.gradle.settingsEvaluated {
val protonLibsUri = when {
config.uri.isPresent -> config.uri.get()
isCI -> "https://gitlab-ci-token:$token@$host/proton/mobile/android/proton-libs.git"
hasHostToken -> "https://gitlab-ci-token:$token@$host/proton/mobile/android/proton-libs.git"
else -> "https://github.com/ProtonMail/protoncore_android.git"
}
// Configuration for Core.