diff --git a/auth/presentation/api/auth-presentation.api b/auth/presentation/api/auth-presentation.api index 7b7e1014f..a285e197f 100644 --- a/auth/presentation/api/auth-presentation.api +++ b/auth/presentation/api/auth-presentation.api @@ -1310,6 +1310,7 @@ public final class me/proton/core/auth/presentation/entity/signup/SubscriptionDe } public abstract interface class me/proton/core/auth/presentation/testing/ProtonTestEntryPoint { + public abstract fun getFeatureFlagRepository ()Lme/proton/core/featureflag/domain/repository/FeatureFlagRepository; public abstract fun getGetAvailablePaymentProviders ()Lme/proton/core/payment/domain/usecase/GetAvailablePaymentProviders; public abstract fun getLoginTestHelper ()Lme/proton/core/auth/domain/testing/LoginTestHelper; } diff --git a/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/testing/ProtonTestEntryPoint.kt b/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/testing/ProtonTestEntryPoint.kt index 5ae7cf51b..0444ffa4c 100644 --- a/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/testing/ProtonTestEntryPoint.kt +++ b/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/testing/ProtonTestEntryPoint.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2021 Proton Technologies AG - * This file is part of Proton Technologies AG and ProtonCore. + * Copyright (c) 2023 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 @@ -23,12 +23,14 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import me.proton.core.auth.domain.testing.LoginTestHelper +import me.proton.core.featureflag.domain.repository.FeatureFlagRepository import me.proton.core.payment.domain.usecase.GetAvailablePaymentProviders @EntryPoint @InstallIn(SingletonComponent::class) @RestrictTo(RestrictTo.Scope.TESTS) interface ProtonTestEntryPoint { + val featureFlagRepository: FeatureFlagRepository val getAvailablePaymentProviders: GetAvailablePaymentProviders val loginTestHelper: LoginTestHelper } diff --git a/coreexample/build.gradle.kts b/coreexample/build.gradle.kts index ca7aed21d..0d0cbdcef 100644 --- a/coreexample/build.gradle.kts +++ b/coreexample/build.gradle.kts @@ -307,6 +307,7 @@ dependencies { androidTestImplementation( project(Module.androidInstrumentedTest), project(Module.paymentIapTest), + project(Module.planTest), project(Module.quark), `android-test-runner`, `hilt-android-testing`, diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/BaseTest.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/BaseTest.kt index 7472c3706..def991f77 100644 --- a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/BaseTest.kt +++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/BaseTest.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2021 Proton Technologies AG - * This file is part of Proton Technologies AG and ProtonCore. + * Copyright (c) 2023 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 @@ -26,6 +26,7 @@ import kotlinx.coroutines.runBlocking import me.proton.android.core.coreexample.Constants import me.proton.android.core.coreexample.MainActivity import me.proton.core.auth.presentation.testing.ProtonTestEntryPoint +import me.proton.core.domain.entity.UserId import me.proton.core.payment.domain.usecase.PaymentProvider import me.proton.core.test.android.instrumented.ProtonTest import me.proton.core.test.android.instrumented.utils.Shell.setupDeviceForAutomation @@ -54,7 +55,8 @@ open class BaseTest( fun login(user: User) { Log.d(testTag, "Login user: ${user.name}") AddAccountRobot().back().verify { accountSwitcherDisplayed() } - authHelper.login(user.name, user.password) + val sessionInfo = authHelper.login(user.name, user.password) + fetchUnleashFeatureFlags(sessionInfo.userId) Log.d(testTag, "Login done.") } @@ -79,6 +81,7 @@ open class BaseTest( fun prepare() { setupDeviceForAutomation(true) authHelper.logoutAll() + fetchUnleashFeatureFlags(userId = null) Plan.Dev.text = Plan.MailPlus.text Plan.Dev.planName = Plan.MailPlus.planName } @@ -95,5 +98,9 @@ open class BaseTest( availablePaymentProviders().filter { it != PaymentProvider.PayPal }.toSet() + + private fun fetchUnleashFeatureFlags(userId: UserId? = null) = runBlocking { + protonTestEntryPoint.featureFlagRepository.getAll(userId) + } } } diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/DynamicExistingPaymentMethodTests.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/DynamicExistingPaymentMethodTests.kt new file mode 100644 index 000000000..f4b5a047a --- /dev/null +++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/DynamicExistingPaymentMethodTests.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2023 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 . + */ + +package me.proton.core.test.android.uitests.tests.medium.payments + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import me.proton.core.domain.entity.AppStore +import me.proton.core.test.quark.data.Card +import me.proton.core.test.quark.data.Plan +import me.proton.core.test.android.robots.payments.ExistingPaymentMethodsRobot +import me.proton.core.test.android.robots.payments.ExistingPaymentMethodsRobot.PaymentMethodElement.paymentMethod +import me.proton.core.test.android.uitests.robot.CoreexampleRobot +import me.proton.core.test.android.uitests.tests.BaseTest +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import me.proton.core.payment.presentation.R +import me.proton.core.plan.test.robot.SubscriptionRobot +import me.proton.core.test.android.instrumented.utils.StringUtils.stringFromResource + +@RunWith(Parameterized::class) +class DynamicExistingPaymentMethodTests( + private val appStore: AppStore, + private val card: Boolean, + private val paypal: Boolean, + private val inApp: Boolean +) : BaseTest() { + + @Before + fun setPaymentMethods() { + quark.setPaymentMethods(appStore, card, paypal, inApp) + } + + @After + fun setDefaults() { + quark.setDefaultPaymentMethods() + } + + @Test + @Ignore("Requires user with paypal account linked") + fun existingPaypalMethodDisplayed() { + login(userWithPaypal) + + CoreexampleRobot().plansUpgrade() + SubscriptionRobot.selectPlan(Plan.Dev) + ExistingPaymentMethodsRobot().verify { + paymentMethodDisplayed("PayPal", userWithPaypal.paypal) + } + } + + @Test + fun existingCreditCardMethodDisplayed() { + login(userWithCard) + + CoreexampleRobot().plansUpgrade() + SubscriptionRobot.selectPlan(Plan.Dev) + ExistingPaymentMethodsRobot().verify { + paymentMethodDisplayed(Card.default.details(), Card.default.name) + if (inApp) { + val context = ApplicationProvider.getApplicationContext() + googlePaymentMethodDisplayed("${context.getString(R.string.payments_method_google)}*") + } + } + } + + @Test + @Ignore("Requires user with paypal account linked") + fun existingCreditCardAndPayPalDisplayed() { + val user = users.getUser { it.paypal.isNotEmpty() && it.cards.isNotEmpty() && !it.isPaid } + val card = user.cards[0] + + login(user) + + CoreexampleRobot().plansUpgrade() + SubscriptionRobot.selectPlan(Plan.Dev) + ExistingPaymentMethodsRobot().verify { + paymentMethodDisplayed(card.details(), card.name) + paymentMethodDisplayed("PayPal", user.paypal) + } + } + + @Test + @Ignore("Requires user with paypal account linked") + fun switchPaymentMethod() { + val user = users.getUser { it.paypal.isNotEmpty() && it.cards.isNotEmpty() && !it.isPaid } + + CoreexampleRobot().plansUpgrade() + SubscriptionRobot.selectPlan(Plan.Dev) + ExistingPaymentMethodsRobot().verify { + paymentMethod(user.paypal).checkIsNotChecked() + paymentMethod(user.cards[0].details()).checkIsChecked() + } + + ExistingPaymentMethodsRobot() + .selectPaymentMethod(user.paypal) + .verify { + paymentMethod(user.paypal).checkIsChecked() + paymentMethod(user.cards[0].details()).checkIsNotChecked() + } + } + + private fun Card.details(): String = stringFromResource( + R.string.payment_cc_list_item, + brand.value, + number.takeLast(4), + expMonth, + expYear + ) + + companion object { + val userWithCard by lazy { quark.seedUserWithCreditCard() } + val userWithPaypal by lazy { users.getUser { it.paypal.isNotEmpty() } } + + @get:Parameterized.Parameters(name = "appStore_{0}_card_{1}_paypal_{2}_inApp_{3}") + @get:JvmStatic + val data = listOf( + // appStore, card, paypal, inApp + arrayOf(AppStore.GooglePlay, true, true, true), + arrayOf(AppStore.GooglePlay, true, true, false), + arrayOf(AppStore.GooglePlay, true, false, true), + arrayOf(AppStore.GooglePlay, true, false, false) + ).toList() + } +} diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/DynamicNewCreditCardTests.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/DynamicNewCreditCardTests.kt new file mode 100644 index 000000000..dfc30e02a --- /dev/null +++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/DynamicNewCreditCardTests.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 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 . + */ + +package me.proton.core.test.android.uitests.tests.medium.payments + +import me.proton.core.domain.entity.AppStore +import me.proton.core.payment.presentation.R +import me.proton.core.plan.test.robot.SubscriptionRobot +import me.proton.core.test.android.robots.payments.AddCreditCardRobot +import me.proton.core.test.android.uitests.robot.CoreexampleRobot +import me.proton.core.test.android.uitests.tests.BaseTest +import me.proton.core.test.quark.data.Plan +import me.proton.core.test.quark.data.User +import org.junit.After +import org.junit.Test + +class DynamicNewCreditCardTests : BaseTest() { + + private val newCreditCardRobot = AddCreditCardRobot() + + @After + fun setDefaults() { + quark.setDefaultPaymentMethods() + } + + private fun goToPlanUpgrade() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = false) + + login(userWithoutCard) + + CoreexampleRobot().plansCurrent() + SubscriptionRobot.selectPlan(Plan.Dev) + } + + @Test + fun backToPlanSelection() { + goToPlanUpgrade() + newCreditCardRobot.close() + SubscriptionRobot.verifyAtLeastOnePlanIsShown() + } + + @Test + fun missingPaymentDetails() { + goToPlanUpgrade() + newCreditCardRobot + .pay() + .verify { + inputErrorDisplayed(R.string.payments_error_card_name, scroll = true) + inputErrorDisplayed(R.string.payments_error_card_number, scroll = true) + inputErrorDisplayed(R.string.payments_error_cvc, scroll = true) + } + } + + @Test + fun invalidCreditCardNumber() { + goToPlanUpgrade() + newCreditCardRobot + .ccname("Test name") + .country() + .selectCountry("Angola") + .expirationDate("1234") + .cvc("123") + .ccnumber("424242424242424") + .postalCode("1234") + .pay() + .verify { errorSnackbarDisplayed("Your card has been declined") } + } + + companion object { + val userWithoutCard: User = quark.userCreate().first + } +} diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/ExistingPaymentMethodTests.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/ExistingPaymentMethodTests.kt index 1afe8357f..4c93d8681 100644 --- a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/ExistingPaymentMethodTests.kt +++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/ExistingPaymentMethodTests.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2021 Proton Technologies AG - * This file is part of Proton Technologies AG and ProtonCore. + * Copyright (c) 2023 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 @@ -36,6 +36,7 @@ import org.junit.runners.Parameterized import me.proton.core.payment.presentation.R import me.proton.core.test.android.instrumented.utils.StringUtils.stringFromResource +@Ignore("Replaced with DynamicExistingPaymentMethodTests") @RunWith(Parameterized::class) class ExistingPaymentMethodTests( private val appStore: AppStore, diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/NewCreditCardTests.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/NewCreditCardTests.kt index 014c04f9f..75253ba34 100644 --- a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/NewCreditCardTests.kt +++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/payments/NewCreditCardTests.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2021 Proton Technologies AG - * This file is part of Proton Technologies AG and ProtonCore. + * Copyright (c) 2023 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 @@ -27,8 +27,10 @@ import me.proton.core.test.android.robots.plans.SelectPlanRobot import me.proton.core.test.android.uitests.robot.CoreexampleRobot import me.proton.core.test.android.uitests.tests.BaseTest import org.junit.After +import org.junit.Ignore import org.junit.Test +@Ignore("Replaced by DynamicNewCreditCardTests") class NewCreditCardTests : BaseTest() { private val newCreditCardRobot = AddCreditCardRobot() diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/CurrentPlanTests.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/CurrentPlanTests.kt index c922bcd8a..d555d9caa 100644 --- a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/CurrentPlanTests.kt +++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/CurrentPlanTests.kt @@ -31,11 +31,13 @@ import me.proton.core.test.android.uitests.robot.CoreexampleRobot import me.proton.core.test.android.uitests.tests.BaseTest import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Test import java.text.DateFormat import java.util.Calendar import java.util.Date +@Ignore("Replaced by CurrentDynamicPlanTests") class CurrentPlanTests : BaseTest() { private fun navigateUserToCurrentPlans(user: User): SelectPlanRobot { quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = false) diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/DynamicCurrentPlanTests.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/DynamicCurrentPlanTests.kt new file mode 100644 index 000000000..e8bdb6860 --- /dev/null +++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/DynamicCurrentPlanTests.kt @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2023 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 . + */ + +package me.proton.core.test.android.uitests.tests.medium.plans + +import android.content.Context +import androidx.core.text.HtmlCompat +import androidx.test.core.app.ApplicationProvider +import me.proton.core.domain.entity.AppStore +import me.proton.core.plan.presentation.R +import me.proton.core.plan.presentation.entity.PlanCycle +import me.proton.core.plan.test.robot.SubscriptionRobot +import me.proton.core.test.android.uitests.robot.CoreexampleRobot +import me.proton.core.test.android.uitests.tests.BaseTest +import me.proton.core.test.quark.data.Plan +import me.proton.core.test.quark.data.User +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.text.DateFormat +import java.util.Calendar +import java.util.Date + +class DynamicCurrentPlanTests : BaseTest() { + private fun navigateUserToCurrentPlans(user: User) { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = false) + login(user) + + CoreexampleRobot().plansCurrent() + } + + @Before + override fun setUp() { + super.setUp() + quark.jailUnban() + } + + @After + fun setDefaults() { + quark.setDefaultPaymentMethods() + } + + @Test + fun userWithFreePlan() { + val user = quark.userCreate().first + navigateUserToCurrentPlans(user) + SubscriptionRobot.apply { + verifySubscriptionIsShown() + verifyUpgradeYourPlanTextIsDisplayed() + verifyAtLeastOnePlanIsShown() + } + } + + @Test + fun userWithPaidPlan() { + val paidUser = User(plan = Plan.Unlimited) + quark.seedNewSubscriberWithCycle(paidUser, PlanCycle.YEARLY.cycleDurationMonths) + navigateUserToCurrentPlans(paidUser) + SubscriptionRobot.apply { + verifySubscriptionIsShown() + verifyCannotManagePlansFromMobile() + verifyNoPaidPlansAreShown() + } + } + + @Test + fun userWithPaidPlanCardAndIAPPayment1month() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = true) + val paidUserCycle1 = User(plan = Plan.MailPlus) + val cycle1 = PlanCycle.OTHER.apply { + cycleDurationMonths = 1 + } + val user = quark.seedNewSubscriberWithCycle(paidUserCycle1, cycle1.cycleDurationMonths) + login(user) + + CoreexampleRobot().plansCurrent() + SubscriptionRobot.apply { + val context = ApplicationProvider.getApplicationContext() + val cycleText = context.resources.getQuantityString( + R.plurals.plans_billing_other_period, + cycle1.cycleDurationMonths, + cycle1.cycleDurationMonths + ) + val calendar = Calendar.getInstance() + calendar.timeInMillis = System.currentTimeMillis() + calendar.add(Calendar.MONTH, 1) + + verifyPlanRenewalDisplayed(context.calendarToDateFormat(calendar.time).toString()) + verifyPlanCycleDisplayed(cycleText) + } + } + + @Test + fun userWithPaidPlanCardAndIAPPayment12months() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = true) + val paidUserCycle12 = User(plan = Plan.MailPlus) + val cycle12 = PlanCycle.OTHER.apply { + cycleDurationMonths = 12 + } + val user = quark.seedNewSubscriberWithCycle(paidUserCycle12, cycle12.cycleDurationMonths) + login(user) + + CoreexampleRobot().plansCurrent() + SubscriptionRobot.apply { + val context = ApplicationProvider.getApplicationContext() + val calendar = Calendar.getInstance() + calendar.timeInMillis = System.currentTimeMillis() + calendar.add(Calendar.YEAR, 1) + + verifyPlanRenewalDisplayed(context.calendarToDateFormat(calendar.time).toString()) + verifyPlanCycleDisplayed(context.getString(R.string.plans_billing_yearly)) + } + } + + @Test + fun userWithPaidPlanCardAndIAPPayment15months() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = true) + val paidUserCycle15 = User(plan = Plan.VpnPlus) + val cycle15 = PlanCycle.OTHER.apply { + cycleDurationMonths = 15 + } + val user = quark.seedNewSubscriberWithCycle(paidUserCycle15, cycle15.cycleDurationMonths) + login(user) + + CoreexampleRobot().plansCurrent() + SubscriptionRobot.apply { + val context = ApplicationProvider.getApplicationContext() + val cycleText = context.resources.getQuantityString( + R.plurals.plans_billing_other_period, + cycle15.cycleDurationMonths, + cycle15.cycleDurationMonths + ) + val calendar = Calendar.getInstance() + calendar.timeInMillis = System.currentTimeMillis() + calendar.add(Calendar.MONTH, 15) + + verifyPlanRenewalDisplayed(context.calendarToDateFormat(calendar.time).toString()) + verifyPlanCycleDisplayed(cycleText) + } + } + + @Test + fun userWithPaidPlanCardAndIAPPayment30months() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = true) + val paidUserCycle30 = User(plan = Plan.VpnPlus) + val cycle30 = PlanCycle.OTHER.apply { + cycleDurationMonths = 30 + } + val user = quark.seedNewSubscriberWithCycle(paidUserCycle30, cycle30.cycleDurationMonths) + login(user) + + CoreexampleRobot().plansCurrent() + SubscriptionRobot.apply { + val context = ApplicationProvider.getApplicationContext() + val cycleText = context.resources.getQuantityString( + R.plurals.plans_billing_other_period, + cycle30.cycleDurationMonths, + cycle30.cycleDurationMonths + ) + val calendar = Calendar.getInstance() + calendar.timeInMillis = System.currentTimeMillis() + calendar.add(Calendar.MONTH, 30) + + verifyPlanRenewalDisplayed(context.calendarToDateFormat(calendar.time).toString()) + verifyPlanCycleDisplayed(cycleText) + } + } + + private fun Context.calendarToDateFormat(date: Date) = HtmlCompat.fromHtml( + String.format( + getString(R.string.plans_renewal_date), + DateFormat.getDateInstance().format(date) + ), + HtmlCompat.FROM_HTML_MODE_LEGACY + ) +} diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/DynamicSelectPlanTests.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/DynamicSelectPlanTests.kt new file mode 100644 index 000000000..3c218598f --- /dev/null +++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/DynamicSelectPlanTests.kt @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023 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 . + */ + +package me.proton.core.test.android.uitests.tests.medium.plans + +import me.proton.core.domain.entity.AppStore +import me.proton.core.paymentiap.test.robot.GoogleIAPRobot +import me.proton.core.plan.test.robot.SubscriptionRobot +import me.proton.core.test.android.robots.CoreRobot +import me.proton.core.test.android.robots.auth.AddAccountRobot +import me.proton.core.test.android.robots.auth.signup.ChooseUsernameRobot +import me.proton.core.test.android.robots.auth.signup.RecoveryMethodsRobot +import me.proton.core.test.android.robots.humanverification.HVRobot +import me.proton.core.test.android.robots.payments.AddCreditCardRobot +import me.proton.core.test.android.uitests.tests.BaseTest +import me.proton.core.test.android.uitests.tests.SmokeTest +import me.proton.core.test.quark.data.Plan.Dev +import me.proton.core.test.quark.data.Plan.Free +import me.proton.core.test.quark.data.User +import org.junit.After +import org.junit.Before +import org.junit.Test + +class DynamicSelectPlanTests : BaseTest() { + @After + fun setDefaults() { + quark.setDefaultPaymentMethods() + } + + @Before + fun goToPlanSelection() { + quark.jailUnban() + + AddAccountRobot() + .createAccount() + .chooseInternalEmail() + .verify { domainInputDisplayed() } + + val user = User() + ChooseUsernameRobot() + .username(user.name) + .next() + .setAndConfirmPassword(user.password) + .skip() + .skipConfirm() + } + + @Test + fun verifyInitialState() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = false) + + SubscriptionRobot.verifyAtLeastOnePlanIsShown() + } + + @Test + fun selectFreeAndCancelHumanVerification() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = false) + + SubscriptionRobot.selectPlan(Free) + HVRobot().apply { + verify { hvElementsDisplayed() } + }.close() + SubscriptionRobot.verifyAtLeastOnePlanIsShown() + } + + @Test + @SmokeTest + fun selectPlusAndCancelPayment() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = false) + + SubscriptionRobot.selectPlan(Dev) + AddCreditCardRobot() + .apply { + verify { + nextPaymentProviderButtonNotDisplayed() + addCreditCardElementsDisplayed() + } + } + .close() + SubscriptionRobot.verifyAtLeastOnePlanIsShown() + } + + @Test + @SmokeTest + fun selectPlusAndCancelPaymentIAPOnly() { + quark.setPaymentMethods(AppStore.GooglePlay, card = false, paypal = false, inApp = true) + + SubscriptionRobot.selectPlan(Dev) + GoogleIAPRobot() + .apply { + verify { + googleIAPElementsDisplayed() + nextPaymentProviderButtonIsNotVisible() + } + } + .close() + SubscriptionRobot.verifyAtLeastOnePlanIsShown() + } + + @Test + @SmokeTest + fun selectPlusAndCancelPaymentIAPAndCard() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = true) + + SubscriptionRobot.selectPlan(Dev) + AddCreditCardRobot() + .apply { + verify { nextPaymentProviderButtonDisplayed() } + } + .switchPaymentProvider() + GoogleIAPRobot() + .apply { + verify { googleIAPElementsDisplayed() } + } + .close() + SubscriptionRobot.verifyAtLeastOnePlanIsShown() + } + + @Test + @SmokeTest + fun selectPlusNoPaymentProvidersAvailable() { + quark.setPaymentMethods(AppStore.GooglePlay, card = false, paypal = false, inApp = false) + SubscriptionRobot.selectPlan(Dev) + SubscriptionRobot.verifyAtLeastOnePlanIsShown() + } +} diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/DynamicUpgradePlanTests.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/DynamicUpgradePlanTests.kt new file mode 100644 index 000000000..b8c6929ba --- /dev/null +++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/DynamicUpgradePlanTests.kt @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2023 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 . + */ + +package me.proton.core.test.android.uitests.tests.medium.plans + +import me.proton.core.domain.entity.AppStore +import me.proton.core.plan.presentation.entity.PlanCycle +import me.proton.core.plan.test.robot.SubscriptionRobot +import me.proton.core.test.android.uitests.robot.CoreexampleRobot +import me.proton.core.test.android.uitests.tests.BaseTest +import me.proton.core.test.android.uitests.tests.SmokeTest +import me.proton.core.test.quark.data.Plan +import me.proton.core.test.quark.data.User +import org.junit.After +import org.junit.Before +import org.junit.Test + +class DynamicUpgradePlanTests : BaseTest() { + private val coreExampleRobot = CoreexampleRobot() + + @Before + override fun setUp() { + super.setUp() + quark.jailUnban() + } + + @After + fun setDefaults() { + quark.setDefaultPaymentMethods() + } + + @Test + @SmokeTest + fun userWithFreePlan() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = false) + val freeUser = quark.userCreate().first + login(freeUser) + + coreExampleRobot.plansUpgrade() + SubscriptionRobot.apply { + togglePlan(Plan.Dev) + verifyCanGetPlan(Plan.Dev) + close() + } + + coreExampleRobot.verify { accountSwitcherDisplayed() } + } + + @Test + fun userWithPaidPlanCardPayment() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = false) + val paidUser = User(plan = Plan.Unlimited) + quark.seedNewSubscriberWithCycle(paidUser, PlanCycle.YEARLY.cycleDurationMonths) + login(paidUser) + + coreExampleRobot.plansUpgrade() + SubscriptionRobot.verifyNoUpgradeAvailable() + } + + @Test + fun userWithPaidPlanCardAndIAPPayment1month() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = true) + val paidUserCycle1 = User(plan = Plan.MailPlus) + val cycle1 = PlanCycle.OTHER.apply { + cycleDurationMonths = 1 + } + val user = quark.seedNewSubscriberWithCycle(paidUserCycle1, cycle1.cycleDurationMonths) + login(user) + + coreExampleRobot.plansUpgrade() + SubscriptionRobot.apply { + verifyNoPaidPlansAreShown() + verifyNoUpgradeAvailable() + } + } + + @Test + fun userWithPaidPlanCardAndIAPPayment12months() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = true) + val paidUserCycle12 = User(plan = Plan.MailPlus) + val cycle12 = PlanCycle.OTHER.apply { + cycleDurationMonths = 12 + } + val user = quark.seedNewSubscriberWithCycle(paidUserCycle12, cycle12.cycleDurationMonths) + login(user) + + coreExampleRobot.plansUpgrade() + SubscriptionRobot.apply { + verifyNoPaidPlansAreShown() + verifyNoUpgradeAvailable() + } + } + + @Test + fun userWithPaidPlanCardAndIAPPayment15months() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = true) + val paidUserCycle15 = User(plan = Plan.VpnPlus) + val cycle15 = PlanCycle.OTHER.apply { + cycleDurationMonths = 15 + } + val user = quark.seedNewSubscriberWithCycle(paidUserCycle15, cycle15.cycleDurationMonths) + login(user) + + coreExampleRobot.plansUpgrade() + SubscriptionRobot.apply { + verifyNoPaidPlansAreShown() + verifyNoUpgradeAvailable() + } + } + + @Test + fun userWithPaidPlanCardAndIAPPayment30months() { + quark.setPaymentMethods(AppStore.GooglePlay, card = true, paypal = false, inApp = true) + val paidUserCycle15 = User(plan = Plan.VpnPlus) + val cycle15 = PlanCycle.OTHER.apply { + cycleDurationMonths = 30 + } + val user = quark.seedNewSubscriberWithCycle(paidUserCycle15, cycle15.cycleDurationMonths) + login(user) + + coreExampleRobot.plansUpgrade() + SubscriptionRobot.apply { + verifyNoPaidPlansAreShown() + verifyNoUpgradeAvailable() + } + } +} diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/SelectPlanForIAPTests.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/SelectPlanForIAPTests.kt index 08e14bc3b..7432aad74 100644 --- a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/SelectPlanForIAPTests.kt +++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/SelectPlanForIAPTests.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2021 Proton Technologies AG - * This file is part of Proton Technologies AG and ProtonCore. + * Copyright (c) 2023 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 @@ -30,8 +30,10 @@ import me.proton.core.test.android.robots.plans.SelectPlanRobot import me.proton.core.test.android.uitests.tests.BaseTest import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Test +@Ignore("Replaced with SelectDynamicPlanTests") class SelectPlanForIAPTests : BaseTest() { private var humanVerificationRobot: HVRobot? = null diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/SelectPlanTests.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/SelectPlanTests.kt index a03bbd05f..bf06aa889 100644 --- a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/SelectPlanTests.kt +++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/SelectPlanTests.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2021 Proton Technologies AG - * This file is part of Proton Technologies AG and ProtonCore. + * Copyright (c) 2023 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 @@ -33,8 +33,10 @@ import me.proton.core.test.android.uitests.tests.BaseTest import me.proton.core.test.android.uitests.tests.SmokeTest import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Test +@Ignore("Replaced with SelectDynamicPlanTests") class SelectPlanTests : BaseTest() { private var humanVerificationRobot: HVRobot? = null diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/UpgradePlanTests.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/UpgradePlanTests.kt index 6782dd80f..8bb0cea3e 100644 --- a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/UpgradePlanTests.kt +++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/plans/UpgradePlanTests.kt @@ -28,8 +28,10 @@ import me.proton.core.test.android.uitests.tests.BaseTest import me.proton.core.test.android.uitests.tests.SmokeTest import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Test +@Ignore("Replaced with DynamicUpgradePlanTests") class UpgradePlanTests : BaseTest() { private val selectPlanRobot = SelectPlanRobot() diff --git a/coreexample/src/androidTestMock/assets/GET/payments/v4/plans/default.json b/coreexample/src/androidTestMock/assets/GET/payments/v4/plans/default.json index 06282a091..d686f7e6e 100644 --- a/coreexample/src/androidTestMock/assets/GET/payments/v4/plans/default.json +++ b/coreexample/src/androidTestMock/assets/GET/payments/v4/plans/default.json @@ -3,7 +3,7 @@ "Plans": { "Type": 1, "Name": "free", - "Title": "Proton Free", + "Title": "Free", "MaxDomains": 0, "MaxAddresses": 1, "MaxCalendars": 1, diff --git a/coreexample/src/main/kotlin/me/proton/android/core/coreexample/CoreExampleApp.kt b/coreexample/src/main/kotlin/me/proton/android/core/coreexample/CoreExampleApp.kt index 0d2768c1a..dfd9a7b4d 100644 --- a/coreexample/src/main/kotlin/me/proton/android/core/coreexample/CoreExampleApp.kt +++ b/coreexample/src/main/kotlin/me/proton/android/core/coreexample/CoreExampleApp.kt @@ -23,6 +23,7 @@ import androidx.startup.AppInitializer import dagger.hilt.android.HiltAndroidApp import me.proton.android.core.coreexample.init.AccountStateHandlerInitializer import me.proton.android.core.coreexample.init.EventManagerInitializer +import me.proton.android.core.coreexample.init.FeatureFlagInitializer import me.proton.android.core.coreexample.init.LoggerInitializer import me.proton.core.keytransparency.presentation.init.KeyTransparencyInitializer import me.proton.android.core.coreexample.init.WorkManagerInitializer @@ -47,6 +48,7 @@ class CoreExampleApp : Application() { initializeComponent(UnAuthSessionFetcherInitializer::class.java) initializeComponent(KeyTransparencyInitializer::class.java) initializeComponent(LoggerInitializer::class.java) + initializeComponent(FeatureFlagInitializer::class.java) } } } diff --git a/coreexample/src/main/kotlin/me/proton/android/core/coreexample/init/FeatureFlagInitializer.kt b/coreexample/src/main/kotlin/me/proton/android/core/coreexample/init/FeatureFlagInitializer.kt new file mode 100644 index 000000000..3148ca5ae --- /dev/null +++ b/coreexample/src/main/kotlin/me/proton/android/core/coreexample/init/FeatureFlagInitializer.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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 . + */ + +package me.proton.android.core.coreexample.init + +import android.content.Context +import androidx.startup.Initializer +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import me.proton.core.featureflag.data.FeatureFlagRefreshStarter + +class FeatureFlagInitializer: Initializer { + override fun create(context: Context) { + EntryPointAccessors.fromApplication( + context.applicationContext, + FeatureFlagInitializerEntryPoint::class.java + ).featureFlagRefreshStarter().start() + } + + override fun dependencies(): List?>> = listOf( + WorkManagerInitializer::class.java + ) + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface FeatureFlagInitializerEntryPoint { + fun featureFlagRefreshStarter(): FeatureFlagRefreshStarter + } +} diff --git a/payment/presentation/src/main/res/layout/activity_billing.xml b/payment/presentation/src/main/res/layout/activity_billing.xml index 263e8d1e7..fb8ebb5b4 100644 --- a/payment/presentation/src/main/res/layout/activity_billing.xml +++ b/payment/presentation/src/main/res/layout/activity_billing.xml @@ -1,6 +1,6 @@