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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ jobs:
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@56b90f209b02bf6d1deae490e9ef18b21a389cd4 # v1

- name: Checkout submodules
run: git submodule update --init --recursive

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate-gradle-wrapper.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: gradle/actions/wrapper-validation@v3
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class EncryptedUserPreference @Inject constructor(val preference: Preference) {

private const val BLANK_USERNAME = "BLANK_USERNAME"
private const val BLANK_USERNAME_GENERATED_DATE = "BLANK_USERNAME_GENERATED_DATE"
private const val AVAILABLE_PLANS = "AVAILABLE_PLANS"
}

private val sharedPreferences: SharedPreferences = preference.accountPreference
Expand Down Expand Up @@ -129,8 +130,19 @@ class EncryptedUserPreference @Inject constructor(val preference: Preference) {
return sharedPreferences.getString(CURRENT_PLAN, "")
}

fun putAvailablePlans(json: String?) {
sharedPreferences.edit()
.putString(AVAILABLE_PLANS, json)
.apply()
}

fun getAvailablePlans(): String? {
return sharedPreferences.getString(AVAILABLE_PLANS, null)
}

fun getCapabilityMultiHop(): Boolean {
return sharedPreferences.getBoolean(USER_MULTI_HOP, false)
// return sharedPreferences.getBoolean(USER_MULTI_HOP, false)
return true
}

fun getPaymentMethod(): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package net.ivpn.core.common.session
along with the IVPN Android app. If not, see <https://www.gnu.org/licenses/>.
*/

import com.google.gson.Gson
import com.wireguard.android.crypto.Keypair
import net.ivpn.core.IVPNApplication
import net.ivpn.core.common.Mapper
Expand All @@ -34,6 +35,7 @@ import net.ivpn.core.rest.HttpClientFactory
import net.ivpn.core.rest.IVPNApi
import net.ivpn.core.rest.RequestListener
import net.ivpn.core.rest.Responses
import net.ivpn.core.rest.data.model.ServicePlan
import net.ivpn.core.rest.data.model.ServiceStatus
import net.ivpn.core.rest.data.model.WireGuard
import net.ivpn.core.rest.data.session.*
Expand Down Expand Up @@ -326,6 +328,9 @@ class SessionController @Inject constructor(
settings.isMultiHopEnabled = false
}
}
serviceStatus.availablePlans?.let {
userPreference.putAvailablePlans(Gson().toJson(it))
}
}

private fun handleWireGuardResponse(wireGuard: WireGuard?, keys: Keypair?) {
Expand Down
6 changes: 0 additions & 6 deletions core/src/main/java/net/ivpn/core/common/utils/StringUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,4 @@ public static String formatTimeUntilResumed(long timeUntilResumed) {
return builder.toString();
}

// public static String formatPlanName(Plan plan) {
// if (plan == null) {
// return "";
// }
// return "IVPN " + plan.toString();
// }
}
36 changes: 36 additions & 0 deletions core/src/main/java/net/ivpn/core/rest/data/model/ServicePlan.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package net.ivpn.core.rest.data.model

/*
IVPN Android app
https://github.com/ivpn/android-app

Created by Oleksandr Mykhailenko.
Copyright (c) 2023 IVPN Limited.

This file is part of the IVPN Android app.

The IVPN Android app 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.

The IVPN Android app 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 the IVPN Android app. If not, see <https://www.gnu.org/licenses/>.
*/

import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName

data class ServicePlan(
@SerializedName("name")
@Expose
val name: String,

@SerializedName("device_limit")
@Expose
val deviceLimit: Int
)
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ public class ServiceStatus {
@SerializedName("device_management")
@Expose
private Boolean deviceManagement;
@SerializedName("available_plans")
@Expose
private List<ServicePlan> availablePlans = null;

public Boolean getIsActive() {
return isActive;
Expand Down Expand Up @@ -125,6 +128,14 @@ public Boolean getDeviceManagement() {
return deviceManagement;
}

public List<ServicePlan> getAvailablePlans() {
return availablePlans;
}

public void setAvailablePlans(List<ServicePlan> availablePlans) {
this.availablePlans = availablePlans;
}

@Override
public String toString() {
return "ServiceStatus{" +
Expand All @@ -137,6 +148,7 @@ public String toString() {
", isOnFreeTrial='" + isOnFreeTrial + '\'' +
", deviceManagement='" + deviceManagement + '\'' +
", capabilities=" + capabilities +
", availablePlans=" + availablePlans +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,12 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
}

// Device Management enabled, Standard plan
if (deviceManagement && plan.equals(Plan.STANDARD) && isAccountNewStyle) {
if (deviceManagement && !plan.equals(Plan.PRO) && isAccountNewStyle) {
return getDmStandardBinding(inflater, container);
}

// Device Management disabled, Standard plan
if (!deviceManagement && plan.equals(Plan.STANDARD) && isAccountNewStyle) {
if (!deviceManagement && !plan.equals(Plan.PRO) && isAccountNewStyle) {
return getStandardBinding(inflater, container);
}

Expand Down Expand Up @@ -171,11 +171,6 @@ private View getLegacyStandardBinding(@NonNull LayoutInflater inflater, @Nullabl
navigator.tryAgain();
}
});
binding.upgradePlan.setOnClickListener(view -> {
if (navigator != null) {
navigator.upgradePlan(upgradeToUrl);
}
});
binding.close.setOnClickListener(view -> {
if (navigator != null) {
navigator.cancel();
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/net/ivpn/core/v2/dialog/Dialogs.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ public enum Dialogs {
REMOVE_KILL_SWITCH(R.string.dialogs_please_note, R.string.dialogs_remove_kill_switch, R.string.dialogs_to_read_more, R.string.dialogs_ok),
WG_CANT_CHANGE_PORT(R.string.dialogs_please_note, R.string.dialogs_wireguard_impossible_change_port, -1, R.string.dialogs_ok),
WG_QUANTUM_RESISTANCE_INFO(R.string.protocol_wg_quantum_resistance, R.string.protocol_wg_quantum_resistance_info, -1, R.string.dialogs_ok),
DEVICE_LOGGED_OUT(R.string.dialogs_device_logged_out_title, R.string.dialogs_device_logged_out_message, -1, R.string.dialogs_ok);

DEVICE_LOGGED_OUT(R.string.dialogs_device_logged_out_title, R.string.dialogs_device_logged_out_message, -1, R.string.dialogs_ok),
ACCOUNT_INACTIVE(R.string.dialogs_account_expired_title, R.string.dialogs_account_expired_msg, -1, R.string.dialogs_ok);
private int titleId;
private int messageId;
private int positiveBtnId;
Expand Down
105 changes: 96 additions & 9 deletions core/src/main/java/net/ivpn/core/v2/signup/Plan.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,106 @@ package net.ivpn.core.common.billing.addfunds
along with the IVPN Android app. If not, see <https://www.gnu.org/licenses/>.
*/

enum class Plan(val skuPath: String, val productName: String) {
PRO("net.ivpn.subscriptions.pro.", "IVPN Pro"),
STANDARD("net.ivpn.subscriptions.standard.", "IVPN Standard");
import net.ivpn.core.rest.data.model.ServicePlan

enum class Plan(
val skuPath: String,
val productName: String,
val title: String,
val description: String
) {
STANDARD(
skuPath = "net.ivpn.subscriptions.standard.",
productName = "IVPN Standard",
title = "IVPN Standard",
description = "IVPN on 5 devices"
),
PLUS(
skuPath = "net.ivpn.subscriptions.plus.",
productName = "IVPN Plus",
title = "IVPN Plus",
description = "IVPN on 10 devices, modDNS, Mailx"
),
PRO(
skuPath = "net.ivpn.subscriptions.pro.",
productName = "IVPN Pro",
title = "IVPN Pro Suite",
description = "IVPN on 10 devices, modDNS, Mailx, Portmaster Pro"
);

companion object {

fun getPlanByProductName(productName: String?): Plan {
for (plan in values()) {
if (plan.productName == productName) {
return plan
}
if (productName == null) return STANDARD

return when {
productName.contains("Plus", ignoreCase = true) -> PLUS
productName.contains("Pro", ignoreCase = true) -> PRO
else -> STANDARD
}
}
}

fun getPlanTitle(): String = title

fun getDeviceLimit(keyword: String, plans: List<ServicePlan>): Int {
return plans.firstOrNull { it.name.contains(keyword, ignoreCase = true) }?.deviceLimit ?: 0
}

fun getStandardDesc(deviceLimit: Int): String = "IVPN on $deviceLimit devices"

fun getPlusDesc(deviceLimit: Int): String = "IVPN on $deviceLimit devices, modDNS, Mailx"

fun getProDesc(deviceLimit: Int): String = "IVPN on $deviceLimit devices, modDNS, Mailx, Portmaster Pro"

fun getPlanDesc(plans: List<ServicePlan> = emptyList()): String {
if (plans.isEmpty()) return description
return when (this) {
STANDARD -> getStandardDesc(getDeviceLimit("Standard", plans))
PLUS -> getPlusDesc(getDeviceLimit("Plus", plans))
PRO -> getProDesc(getDeviceLimit("Pro", plans))
}
}

fun isStandard(): Boolean = this == STANDARD

fun getAltTitleOne(): String =
when (this) {
STANDARD -> PLUS.title
PLUS -> STANDARD.title
PRO -> STANDARD.title
}

fun getAltDescOne(plans: List<ServicePlan> = emptyList()): String {
if (plans.isEmpty()) return when (this) {
STANDARD -> PLUS.description
PLUS -> STANDARD.description
PRO -> STANDARD.description
}
return when (this) {
STANDARD -> getPlusDesc(getDeviceLimit("Plus", plans))
PLUS -> getStandardDesc(getDeviceLimit("Standard", plans))
PRO -> getStandardDesc(getDeviceLimit("Standard", plans))
}
}

fun getAltTitleTwo(): String =
when (this) {
STANDARD -> PRO.title
PLUS -> PRO.title
PRO -> PLUS.title
}

return STANDARD
fun getAltDescTwo(plans: List<ServicePlan> = emptyList()): String {
if (plans.isEmpty()) return when (this) {
STANDARD -> PRO.description
PLUS -> PRO.description
PRO -> PLUS.description
}
return when (this) {
STANDARD -> getProDesc(getDeviceLimit("Pro", plans))
PLUS -> getProDesc(getDeviceLimit("Pro", plans))
PRO -> getPlusDesc(getDeviceLimit("Plus", plans))
}
}
}
}
29 changes: 26 additions & 3 deletions core/src/main/java/net/ivpn/core/v2/viewmodel/AccountViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@ import androidx.databinding.ObservableBoolean
import androidx.databinding.ObservableField
import androidx.databinding.ObservableLong
import androidx.lifecycle.ViewModel
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import net.ivpn.core.IVPNApplication
import net.ivpn.core.common.billing.addfunds.Plan
import net.ivpn.core.common.dagger.ApplicationScope
import net.ivpn.core.common.prefs.EncryptedUserPreference
import net.ivpn.core.common.qr.QRController
import net.ivpn.core.common.session.SessionController
import net.ivpn.core.common.session.SessionListenerImpl
import net.ivpn.core.common.utils.DateUtil
import net.ivpn.core.rest.data.model.ServicePlan
import net.ivpn.core.rest.data.session.SessionErrorResponse
import org.slf4j.LoggerFactory
import javax.inject.Inject
Expand Down Expand Up @@ -65,6 +69,8 @@ class AccountViewModel @Inject constructor(
val isActive = ObservableBoolean()
val deviceManagement = ObservableBoolean()
val deviceName = ObservableField<String>()
val plan = ObservableField<Plan>()
val availablePlans = ObservableField<List<ServicePlan>>(emptyList())

val isExpired = ObservableBoolean()
val isExpiredIn = ObservableBoolean()
Expand Down Expand Up @@ -105,6 +111,8 @@ class AccountViewModel @Inject constructor(
paymentMethod = getPaymentMethodValue()
deviceManagement.set(getDeviceManagement())
deviceName.set(getDeviceName())
plan.set(getPlanByProductName(accountType.get()))
availablePlans.set(getAvailablePlansValue())

updateExpireData()
}
Expand Down Expand Up @@ -178,17 +186,22 @@ class AccountViewModel @Inject constructor(
}

fun isAccountStandard(): Boolean {
return accountType.get()?.equals("IVPN Standard") ?: false
return plan.get() == Plan.STANDARD
}

fun isAccountLegacy(): Boolean {
return accountType.get()?.equals("Member VPN Pro Account") ?: false
fun isAccountLegacyTeam(): Boolean {
val user = username.get() ?: return false
return user.startsWith("ivpn") && accountType.get()?.contains("Member") == true
}

fun isAccountNewStyle(): Boolean {
return isNewStyleAccount(username.get().toString())
}

fun showAddMoreTime(): Boolean {
return isAccountStandard() && isAccountNewStyle()
}

private fun clearLocalCache() {
authenticated.set(false)
}
Expand Down Expand Up @@ -281,6 +294,16 @@ class AccountViewModel @Inject constructor(
return userPreference.getDeviceName()
}

private fun getPlanByProductName(productName: String?): Plan {
return Plan.getPlanByProductName(productName)
}

private fun getAvailablePlansValue(): List<ServicePlan> {
val json = userPreference.getAvailablePlans() ?: return emptyList()
val type = object : TypeToken<List<ServicePlan>>() {}.type
return Gson().fromJson(json, type) ?: emptyList()
}

interface AccountNavigator {
fun onLogOut()

Expand Down
Loading
Loading