diff --git a/android/app/build.gradle b/android/app/build.gradle index 436c5d9e5..971c17011 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,17 +1,18 @@ plugins { id 'com.android.application' id 'kotlin-android' + id 'kotlinx-serialization' id 'dev.flutter.flutter-gradle-plugin' id 'org.jlleitschuh.gradle.ktlint' id 'org.jetbrains.kotlin.android' id 'kotlin-parcelize' id 'kotlin-kapt' id 'com.google.protobuf' - id "io.sentry.android.gradle" version "4.2.0" + id "io.sentry.android.gradle" version "4.9.0" } android { - ndkVersion "26.0.10792818" + ndkVersion "26.1.10909125" bundle { density { @@ -367,6 +368,8 @@ configurations { dependencies { implementation "org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3.5" implementation "com.google.protobuf:protobuf-javalite:$protoc_version" + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1' + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugarJdk" implementation fileTree(dir: "libs", include: "liblantern-${androidArch()}.aar") implementation fileTree(dir: 'libs', include: '*.jar') @@ -393,8 +396,8 @@ dependencies { // from https://github.com/getlantern/opus_android implementation files('libs/opuslib-release.aar') implementation 'com.github.getlantern:secrets-android:f6a7a69f3d' - implementation 'com.github.getlantern:db-android:5a082e0bdd' - implementation 'com.github.getlantern:messaging-android:ca369d5173' + implementation 'com.github.getlantern:db-android:2e81ba6542' + implementation 'com.github.getlantern:messaging-android:3f23f07b4e' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' //Test implementation diff --git a/android/app/src/main/java/org/getlantern/lantern/model/ProPlan.java b/android/app/src/main/java/org/getlantern/lantern/model/ProPlan.java deleted file mode 100644 index fd5ac1c88..000000000 --- a/android/app/src/main/java/org/getlantern/lantern/model/ProPlan.java +++ /dev/null @@ -1,363 +0,0 @@ -package org.getlantern.lantern.model; - -import android.content.Context; -import android.text.TextUtils; - -import com.google.gson.annotations.SerializedName; - -import org.getlantern.lantern.R; -import org.getlantern.mobilesdk.Logger; - -import java.util.ArrayList; -import java.util.Currency; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -public class ProPlan { - private static final String TAG = ProPlan.class.getName(); - - @SerializedName("id") - private String id; - @SerializedName("description") - private String description; - @SerializedName("bestValue") - private boolean bestValue; - @SerializedName("duration") - private Map duration; - @SerializedName("price") - private Map price; - private Map priceWithoutTax; - private Map tax; - @SerializedName("usdPrice") - private Long usdEquivalentPrice; - @SerializedName("renewalBonusExpected") - private Map renewalBonusExpected; - @SerializedName("expectedMonthlyPrice") - private Map expectedMonthlyPrice; - @SerializedName("discount") - private float discount; - @SerializedName("level") - private String level; - - private String currencyCode; - private String costStr; - private String costWithoutTaxStr; - private String taxStr; - private Locale locale = Locale.getDefault(); - private String renewalText; - private String totalCost; - private String totalCostBilledOneTime; - private String formattedBonus; - private String oneMonthCost; - private String formattedDiscount; - - private static final String PLAN_COST = "%1$s%2$s"; - private static final String defaultCurrencyCode = "usd"; - - public ProPlan() { - // default constructor - } - - public ProPlan(String id, Map price, Map priceWithoutTax, - boolean bestValue, Map duration) { - this.id = id; - this.price = price; - this.priceWithoutTax = priceWithoutTax; - this.tax = new HashMap<>(); - this.renewalBonusExpected = new HashMap<>(); - this.bestValue = bestValue; - this.duration = duration; - for (Map.Entry entry : this.price.entrySet()) { - String currency = entry.getKey(); - Long priceWithTax = entry.getValue(); - Long specificPriceWithoutTax = priceWithoutTax.get(currency); - if (specificPriceWithoutTax == null) { - specificPriceWithoutTax = priceWithTax; - } - long tax = priceWithTax - specificPriceWithoutTax; - if (tax > 0) { - this.tax.put(currency, tax); - } - } - calculateExpectedMonthlyPrice(); - this.formatCost(); // this will set the currency code for us - } - - public void updateRenewalBonusExpected(Map renewalBonusExpected) { - this.renewalBonusExpected = renewalBonusExpected; - calculateExpectedMonthlyPrice(); - } - - public Map getRenewalBonusExpected() { - return renewalBonusExpected; - } - - /** - * The formula in here matches the calculation in the pro-servers /plans endpoint - */ - private void calculateExpectedMonthlyPrice() { - this.expectedMonthlyPrice = new HashMap<>(); - final Integer monthsPerYear = 12; - final Integer daysPerMonth = 30; - Integer bonusMonths = renewalBonusExpected.get("months"); - Integer bonusDays = renewalBonusExpected.get("days"); - if (bonusMonths == null) { - bonusMonths = 0; - } - if (bonusDays == null) { - bonusDays = 0; - } - Double expectedMonths = (numYears() * monthsPerYear) + bonusMonths + (bonusDays.doubleValue() / daysPerMonth.doubleValue()); - for (Map.Entry entry : this.price.entrySet()) { - String currency = entry.getKey(); - Long priceWithTax = entry.getValue(); - this.expectedMonthlyPrice.put(currency, Double.valueOf(priceWithTax / expectedMonths).longValue()); - } - } - - public Integer numYears() { - return duration.get("years"); - } - - public String formatRenewalBonusExpected(Context context) { - Integer bonusMonths = renewalBonusExpected.get("months"); - Integer bonusDays = renewalBonusExpected.get("days"); - List bonusParts = new ArrayList<>(); - if (bonusMonths != null && bonusMonths > 0) { - bonusParts.add(context.getResources().getQuantityString(R.plurals.month, bonusMonths, bonusMonths)); - } - if (bonusDays != null && bonusDays > 0) { - bonusParts.add(context.getResources().getQuantityString(R.plurals.day, bonusDays, bonusDays)); - } - return TextUtils.join(" ", bonusParts); - } - - @SerializedName("renewalText") - public void setRenewalText(final String renewalText) { - this.renewalText = renewalText; - } - - public String getRenewalText() { - return renewalText; - } - - @SerializedName("totalCost") - public void setTotalCost(final String totalCost) { - this.totalCost = totalCost; - } - - public String getTotalCost() { - return totalCost; - } - - @SerializedName("totalCost") - public void setTotalCostBilledOneTime(final String totalCostBilledOneTime) { - this.totalCostBilledOneTime = totalCostBilledOneTime; - } - - public String getTotalCostBilledOneTime() { - return totalCostBilledOneTime; - } - - @SerializedName("formattedBonus") - public void setFormattedBonus(final String formattedBonus) { - this.formattedBonus = formattedBonus; - } - - public String getFormattedBonus() { - return formattedBonus; - } - - @SerializedName("oneMonthCost") - public void setOneMonthCost(final String oneMonthCost) { - this.oneMonthCost = oneMonthCost; - } - - public String getOneMonthCost() { - return oneMonthCost; - } - - @SerializedName("formattedDiscount") - public void setFormattedDiscount(final String formattedDiscount) { - this.formattedDiscount = formattedDiscount; - } - - public Map getPrice() { - return price; - } - - public Map getDuration() { - return duration; - } - - public Long getCurrencyPrice() { - return price.get(currencyCode); - } - - public Long getUSDEquivalentPrice() { - return usdEquivalentPrice; - } - - @SerializedName("price") - public void setPrice(final Map price) { - this.price = price; - } - - @SerializedName("duration") - public void setDuration(final Map duration) { - this.duration = duration; - } - - public String toString() { - return String.format("Plan: %s Description: %s Num Years %d", - id, description, numYears()); - } - - public void setLocale(Locale locale) { - this.locale = locale; - } - - public Boolean getBestValue() { - return bestValue; - } - - public Locale getLocale() { - return locale; - } - - public String getId() { - return id; - } - - public String getDescription() { - return description; - } - - public void setDescription(String desc) { - description = desc; - } - - public String getCostStr() { - return costStr; - } - - public String getCostWithoutTaxStr() { - return costWithoutTaxStr; - } - - public String getTaxStr() { - return taxStr; - } - - public String getCurrency() { - return currencyCode; - } - - public Currency getCurrencyObj() { - Currency currency = null; - try { - currency = currencyForCode(currencyCode); - } catch (IllegalArgumentException iae) { - Logger.error(TAG, "Possibly invalid currency code: " + currencyCode + ": " + iae.getMessage()); - } - if (currency == null) { - currency = currencyForCode(defaultCurrencyCode); - } - return currency; - } - - private Currency currencyForCode(String currencyCode) { - try { - if (currencyCode != null) { - // It seems that older Android versions require this to be upper case - currencyCode = currencyCode.toUpperCase(); - } - return Currency.getInstance(currencyCode); - } catch (IllegalArgumentException iae) { - throw new IllegalArgumentException("Unable to get currency for " + currencyCode + ": " + iae.getMessage(), iae.getCause()); - } - } - - public String getFormattedPrice() { - return getFormattedPrice(price); - } - - public String getFormattedPriceOneMonth() { - return getFormattedPrice(expectedMonthlyPrice, true); - } - - public String getFormattedPrice(Map price) { - return getFormattedPrice(price, false); - } - - private String getFormattedPrice(Map price, boolean formatFloat) { - final String formattedPrice; - Long currencyPrice = price.get(currencyCode); - if (currencyPrice == null) { - return ""; - } - if (currencyCode.equalsIgnoreCase("irr")) { - if (formatFloat) { - formattedPrice = Utils.convertEasternArabicToDecimalFloat(currencyPrice / 100f); - } else { - formattedPrice = Utils.convertEasternArabicToDecimal(currencyPrice / 100); - } - } else { - if (formatFloat) { - formattedPrice = String.format(Locale.getDefault(), "%.2f", currencyPrice / 100f); - } else { - formattedPrice = String.valueOf(currencyPrice / 100); - } - } - return String.format(PLAN_COST, getSymbol(), formattedPrice); - } - - public String getSymbol() { - final Currency currency = getCurrencyObj(); - return currency.getSymbol(); - } - - public void formatCost() { - if (price == null || price.entrySet() == null) { - return; - } - Map.Entry entry = price.entrySet().iterator().next(); - this.currencyCode = entry.getKey(); - this.costStr = getFormattedPrice(price); - if (priceWithoutTax != null) { - this.costWithoutTaxStr = getFormattedPrice(priceWithoutTax); - this.taxStr = getFormattedPrice(tax); - } else { - this.costWithoutTaxStr = this.costStr; - } - } - - public String getFormatPriceWithBonus(Context context, boolean useNumber) { - String durationFormat; - if (useNumber) { - durationFormat = context.getString(R.string.plan_duration, numYears()); - } else { - if (numYears() == 1) { - durationFormat = context.getString(R.string.one_year_lantern_pro); - } else { - durationFormat = context.getString(R.string.two_years_lantern_pro); - } - } - - String bonus = formatRenewalBonusExpected(context); - if (!bonus.isEmpty()) { - durationFormat += " + " + formatRenewalBonusExpected(context); - } - return durationFormat; - } - - public boolean isBestValue() { - return bestValue; - } - - public float getDiscount() { - return discount; - } -} diff --git a/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt b/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt index ca8d64fd6..9295e6d8f 100644 --- a/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt +++ b/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt @@ -351,7 +351,6 @@ class SessionModel( paymentMethods: List, ) { - LanternApp.getSession().setUserPlans(activity, proPlans) LanternApp.getSession().setPaymentMethods(paymentMethods) } diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt b/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt index 7831e55ff..0aacc547c 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt @@ -87,57 +87,14 @@ class MainActivity : override fun onListen(event: Event) { if (LanternApp.getSession().lanternDidStart()) { Plausible.init(applicationContext) - Logger.debug(TAG, "Plausible initialized") Plausible.enable(true) + Logger.debug(TAG, "Plausible initialized") fetchLoConf() - Logger.debug( - TAG, - "fetchLoConf() finished at ${System.currentTimeMillis() - start}", - ) + updateUserAndPaymentData() } LanternApp.getSession().dnsDetector.publishNetworkAvailability() } } - EventHandler.subscribeAppEvents { appEvent -> - when (appEvent) { - is AppEvent.AccountInitializationEvent -> onInitializingAccount(appEvent.status) - is AppEvent.BandwidthEvent -> { - val event = appEvent as AppEvent.BandwidthEvent - Logger.debug("bandwidth updated", event.bandwidth.toString()) - vpnModel.updateBandwidth(event.bandwidth) - } - is AppEvent.LoConfEvent -> { - doProcessLoconf(appEvent.loconf) - } - is AppEvent.LocaleEvent -> { - // Recreate the activity when the language changes - recreate() - } - is AppEvent.StatsEvent -> { - val stats = appEvent.stats - Logger.debug("Stats updated", stats.toString()) - sessionModel.saveServerInfo( - Vpn.ServerInfo - .newBuilder() - .setCity(stats.city) - .setCountry(stats.country) - .setCountryCode(stats.countryCode) - .build(), - ) - } - is AppEvent.StatusEvent -> { - updateUserData() - updatePaymentMethods() - updateCurrencyList() - } - is AppEvent.VpnStateEvent -> { - updateStatus(appEvent.vpnState.useVpn) - } - else -> { - Logger.debug(TAG, "Unknown app event " + appEvent) - } - } - } MethodChannel( flutterEngine.dartExecutor.binaryMessenger, "lantern_method_channel", @@ -174,6 +131,7 @@ class MainActivity : val intent = Intent(this, LanternService_::class.java) context.startService(intent) Logger.debug(TAG, "startService finished at ${System.currentTimeMillis() - start}") + subscribeAppEvents() } override fun onNewIntent(intent: Intent) { @@ -183,6 +141,53 @@ class MainActivity : navigateForIntent(intent) } + private fun subscribeAppEvents() { + EventHandler.subscribeAppEvents { appEvent -> + when (appEvent) { + is AppEvent.AccountInitializationEvent -> onInitializingAccount(appEvent.status) + is AppEvent.BandwidthEvent -> { + val event = appEvent as AppEvent.BandwidthEvent + Logger.debug("bandwidth updated", event.bandwidth.toString()) + vpnModel.updateBandwidth(event.bandwidth) + } + is AppEvent.LoConfEvent -> { + doProcessLoconf(appEvent.loconf) + } + is AppEvent.LocaleEvent -> { + // Recreate the activity when the language changes + recreate() + } + is AppEvent.StatsEvent -> { + val stats = appEvent.stats + Logger.debug("Stats updated", stats.toString()) + sessionModel.saveServerInfo( + Vpn.ServerInfo + .newBuilder() + .setCity(stats.city) + .setCountry(stats.country) + .setCountryCode(stats.countryCode) + .build(), + ) + } + is AppEvent.StatusEvent -> { + updateUserAndPaymentData() + } + is AppEvent.VpnStateEvent -> { + updateStatus(appEvent.vpnState.useVpn) + } + else -> { + Logger.debug(TAG, "Unknown app event " + appEvent) + } + } + } + } + + private fun updateUserAndPaymentData() { + updateUserData() + updatePaymentMethods() + updateCurrencyList() + } + private fun navigateForIntent(intent: Intent) { // handles text messaging intent intent.getByteArrayExtra("contactForConversation")?.let { contact -> diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/model/Device.kt b/android/app/src/main/kotlin/org/getlantern/lantern/model/Device.kt index d169d8733..242941257 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/model/Device.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/model/Device.kt @@ -1,5 +1,8 @@ package org.getlantern.lantern.model +import kotlinx.serialization.Serializable + +@Serializable data class Device( val id: String, val name: String, diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/model/InAppBilling.kt b/android/app/src/main/kotlin/org/getlantern/lantern/model/InAppBilling.kt index ec812afb5..8e87652dc 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/model/InAppBilling.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/model/InAppBilling.kt @@ -203,6 +203,7 @@ class InAppBilling( it.oneTimePurchaseOfferDetails!!.priceAmountMicros / 10000 val proModel = ProPlan( id, + it.description, hashMapOf(currency to price.toLong()), hashMapOf(currency to priceWithoutTax.toLong()), "2" == years, diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/model/LanternSessionManager.kt b/android/app/src/main/kotlin/org/getlantern/lantern/model/LanternSessionManager.kt index c54c37af4..2e5e84c77 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/model/LanternSessionManager.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/model/LanternSessionManager.kt @@ -5,7 +5,6 @@ import android.content.Context import android.content.res.Resources import android.os.Build import io.lantern.model.Vpn -import org.getlantern.lantern.util.PlansUtil import org.getlantern.mobilesdk.Logger import org.getlantern.mobilesdk.model.SessionManager import org.joda.time.LocalDateTime @@ -308,11 +307,11 @@ class LanternSessionManager(application: Application) : SessionManager(applicati fun storeUserData(user: ProUser) { Logger.debug(TAG, "Storing user data $user") - if (!user.email.isNullOrEmpty()) { + if (user.email.isNotEmpty()) { setEmail(user.email) } - if (!user.code.isNullOrEmpty()) { + if (user.code.isNotEmpty()) { setCode(user.code) } @@ -324,14 +323,16 @@ class LanternSessionManager(application: Application) : SessionManager(applicati setExpired(user.isExpired) setIsProUser(user.isProUser) - val devices = Vpn.Devices.newBuilder().addAllDevices( - user.devices.map { - Vpn.Device.newBuilder().setId(it.id) - .setName(it.name).setCreated(it.created).build() - }, - ).build() - db.mutate { tx -> - tx.put(DEVICES, devices) + if (user.devices.isNotEmpty()) { + val devices = Vpn.Devices.newBuilder().addAllDevices( + user.devices.map { + Vpn.Device.newBuilder().setId(it.id) + .setName(it.name).setCreated(it.created).build() + }, + ).build() + db.mutate { tx -> + tx.put(DEVICES, devices) + } } if (user.isProUser) { @@ -349,10 +350,6 @@ class LanternSessionManager(application: Application) : SessionManager(applicati } fun setUserPlans(context: Context, proPlans: Map) { - for (planId in proPlans.keys) { - proPlans[planId]?.let { PlansUtil.updatePrice(context, it) } - } - plans.clear() plans.putAll(proPlans) db.mutate { tx -> @@ -360,7 +357,6 @@ class LanternSessionManager(application: Application) : SessionManager(applicati try { val planID = it.id.substringBefore('-') val path = PLANS + planID - val planItem = Vpn.Plan.newBuilder().setId(it.id) .setDescription(it.description).setBestValue(it.bestValue) .putAllPrice(it.price).setTotalCostBilledOneTime(it.totalCostBilledOneTime) diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/model/PaymentMethod.kt b/android/app/src/main/kotlin/org/getlantern/lantern/model/PaymentMethod.kt index cee475dc8..5035907c9 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/model/PaymentMethod.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/model/PaymentMethod.kt @@ -1,40 +1,43 @@ package org.getlantern.lantern.model -import com.google.gson.annotations.SerializedName - +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerialName +@Serializable enum class PaymentMethod(val method: String) { - @SerializedName("credit-card") + @SerialName("credit-card") CreditCard("credit-card"), - @SerializedName("unionpay") + @SerialName("unionpay") UnionPay("unionpay"), - @SerializedName("alipay") + @SerialName("alipay") Alipay("alipay"), - @SerializedName("btc") + @SerialName("btc") BTC("btc"), - @SerializedName("wechatpay") + @SerialName("wechatpay") WeChatPay("wechatpay"), - @SerializedName("freekassa") + @SerialName("freekassa") Freekassa("freekassa"), - @SerializedName("paymentwall") + @SerialName("paymentwall") PaymentWall("paymentwall"), - @SerializedName("fropay") + @SerialName("fropay") FroPay("fropay") } +@Serializable data class PaymentMethods( - @SerializedName("method") var method: PaymentMethod, - @SerializedName("providers") var providers: List, + @SerialName("method") var method: PaymentMethod, + @SerialName("providers") var providers: List, ) +@Serializable data class Icons( - @SerializedName("paymentwall") val paymentwall: List, - @SerializedName("stripe") val stripe: List + @SerialName("paymentwall") val paymentwall: List, + @SerialName("stripe") val stripe: List ) diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/model/ProPlan.kt b/android/app/src/main/kotlin/org/getlantern/lantern/model/ProPlan.kt new file mode 100644 index 000000000..f68ca4c77 --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/model/ProPlan.kt @@ -0,0 +1,65 @@ +package org.getlantern.lantern.model + +import java.util.Currency +import java.util.Locale +import kotlinx.serialization.Serializable +import org.getlantern.mobilesdk.Logger + +@Serializable +data class ProPlan( + val id: String, + var description: String, + var price: MutableMap = mutableMapOf(), + var priceWithoutTax: MutableMap = mutableMapOf(), + var bestValue: Boolean = false, + var duration: MutableMap = mutableMapOf(), + var discount: Double = 0.0, + var renewalBonusExpected: MutableMap = mutableMapOf(), + var expectedMonthlyPrice: MutableMap = mutableMapOf(), + var renewalText: String = "", + var totalCost: String = "", + var currencyCode: String = "", + var oneMonthCost: String = "" +) { + var currency = currencyCode + var formattedBonus: String = "" + var formattedDiscount: String = "" + var costStr: String = "" + var costWithoutTaxStr: String? = null + var totalCostBilledOneTime: String = "" + + fun formattedCost(costs: MutableMap, formatFloat:Boolean = false):String { + if (currencyCode == "" || costs.get(currencyCode) == null) return "" + val currencyPrice:Long = costs.get(currencyCode)!! + var formattedPrice = "" + formattedPrice = when (currencyCode.lowercase()) { + "irr" -> if (formatFloat) Utils.convertEasternArabicToDecimalFloat(currencyPrice / 100f) else Utils.convertEasternArabicToDecimal(currencyPrice / 100) + else -> if (formatFloat) String.format(Locale.getDefault(), "%.2f", currencyPrice / 100f) else (currencyPrice / 100).toString() + } + return String.format("%s%s", symbol(), formattedPrice) + } + + fun formattedPrice() = formattedCost(price) + + fun formattedPriceOneMonth() = formattedCost(expectedMonthlyPrice, true) + + fun formatCost() { + if (currencyCode.isNullOrEmpty() && price.size > 0) this.currencyCode = price.keys.first() + this.costStr = formattedCost(price) + if (priceWithoutTax.size > 0) { + this.costWithoutTaxStr = formattedCost(priceWithoutTax) + } else { + this.costWithoutTaxStr = costStr + } + } + + private fun currencyObject(): Currency? { + return if (currencyCode.isNullOrEmpty()) null else Currency.getInstance(currencyCode) + } + + private fun symbol(): String { + return currencyObject()?.let { + return it.symbol + } ?: "" + } +} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/model/ProUser.kt b/android/app/src/main/kotlin/org/getlantern/lantern/model/ProUser.kt index 6a6b9a0fd..9e020f1fe 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/model/ProUser.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/model/ProUser.kt @@ -3,18 +3,21 @@ package org.getlantern.lantern.model import org.joda.time.Days import org.joda.time.LocalDateTime import org.joda.time.Months +import kotlinx.serialization.Serializable +@Serializable data class ProUser( val userId: Long, val token: String, - val referral: String, - val email: String, - val userStatus: String, - val code: String, - val subscription: String, - val expiration: Long, - val devices: List, - val userLevel: String, + val referral: String = "", + val email: String = "", + val userStatus: String = "", + val code: String = "", + val locale: String = "", + val subscription: String = "", + val expiration: Long = 0, + val devices: List = mutableListOf(), + var userLevel: String = "", ) { private fun isUserStatus(status: String) = userStatus == status diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/model/ProviderInfo.kt b/android/app/src/main/kotlin/org/getlantern/lantern/model/ProviderInfo.kt index 6ff37415f..ee18c075d 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/model/ProviderInfo.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/model/ProviderInfo.kt @@ -1,34 +1,37 @@ package org.getlantern.lantern.model -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerialName +@Serializable enum class PaymentProvider(val provider: String) { - @SerializedName("stripe") + @SerialName("stripe") Stripe("stripe"), - @SerializedName("freekassa") + @SerialName("freekassa") Freekassa("freekassa"), - @SerializedName("googleplay") + @SerialName("googleplay") GooglePlay("googleplay"), - @SerializedName("btcpay") + @SerialName("btcpay") BTCPay("btcpay"), - @SerializedName("reseller-code") + @SerialName("reseller-code") ResellerCode("reseller-code"), - @SerializedName("paymentwall") + @SerialName("paymentwall") PaymentWall("paymentwall"), - @SerializedName("fropay") + @SerialName("fropay") Fropay("fropay") } +@Serializable data class ProviderInfo( - @SerializedName("name") var name: PaymentProvider, - @SerializedName("data") var data: Map, - var logoUrl: List + var name: PaymentProvider, + var data: Map = mapOf(), + var logoUrl: List = listOf() ) fun String.toPaymentProvider(): PaymentProvider? { diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/JsonUtil.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/JsonUtil.kt new file mode 100644 index 000000000..a48dd1ca8 --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/JsonUtil.kt @@ -0,0 +1,33 @@ +package org.getlantern.lantern.util + +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.getlantern.mobilesdk.Logger + +object JsonUtil { + + val json = Json { + encodeDefaults = true + ignoreUnknownKeys = true + explicitNulls = false + prettyPrint = true + } + + inline fun fromJson(s: String): T { + return json.decodeFromString(s) + } + + inline fun toJson(obj: T): String { + return json.encodeToString(obj) + } + + inline fun tryParseJson(s: String?): T? { + return try { + fromJson(s ?: return null) + } catch (e: Exception) { + Logger.error("JsonUtil", "Unable to parse JSON", e) + null + } + } +} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt index dfbad9757..8caa47b47 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt @@ -1,9 +1,8 @@ package org.getlantern.lantern.model -import com.google.gson.Gson import com.google.gson.JsonObject import com.google.gson.JsonParser -import com.google.gson.reflect.TypeToken +import kotlinx.serialization.SerializationException import okhttp3.CacheControl import okhttp3.Call import okhttp3.Callback @@ -18,8 +17,8 @@ import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import org.getlantern.lantern.LanternApp -import org.getlantern.lantern.service.LanternService -import org.getlantern.lantern.util.Json +import org.getlantern.lantern.util.JsonUtil +import org.getlantern.lantern.util.PlansUtil import org.getlantern.mobilesdk.Logger import org.getlantern.mobilesdk.util.HttpClient import java.io.IOException @@ -52,10 +51,6 @@ open class LanternHttpClient : HttpClient() { proRequest("POST", url, userHeaders(), body, cb) } - inline fun parseData(row: String): T { - return Gson().fromJson(row, object : TypeToken() {}.type) - } - fun createUser(cb: ProUserCallback) { val formBody = FormBody.Builder() @@ -69,14 +64,13 @@ open class LanternHttpClient : HttpClient() { } override fun onSuccess(response: Response?, result: JsonObject?) { - val user: ProUser? = Json.gson.fromJson(result, ProUser::class.java) - if (user == null) { - Logger.error(TAG, "Unable to parse user from JSON") - return + result?.let { + val user: ProUser? = JsonUtil.tryParseJson(result.toString()) + user?.let { + cb.onSuccess(response!!, user) + } } - cb.onSuccess(response!!, user) } - }) } @@ -99,9 +93,8 @@ open class LanternHttpClient : HttpClient() { ) { Logger.debug(TAG, "JSON response" + result.toString()) result?.let { - val user = parseData(result.toString()) - Logger.debug(TAG, "User ID is ${user.userId}") - cb.onSuccess(response!!, user) + var user: ProUser? = JsonUtil.tryParseJson(result.toString()) + user?.let { cb.onSuccess(response!!, it) } } } }, @@ -142,7 +135,7 @@ open class LanternHttpClient : HttpClient() { val plans = mutableMapOf() for (plan in fetched) { plan.formatCost() - plans.put(plan.id, plan) + plans.put(plan.id, PlansUtil.updatePrice(LanternApp.getAppContext(), plan)) } return plans } @@ -173,10 +166,10 @@ open class LanternHttpClient : HttpClient() { ) { Logger.d(TAG, "Plans v3 Response body $result") val methods = - parseData>>( + JsonUtil.fromJson>>( result?.get("providers").toString(), ) - val icons = parseData(result?.get("icons").toString()) + val icons = JsonUtil.fromJson(result?.get("icons").toString()) Logger.d(TAG, "Plans v3 Icons Response body $icons") val providers = methods.get("android") // Due to API limitations @@ -194,7 +187,7 @@ open class LanternHttpClient : HttpClient() { } } } - val fetched = parseData>(result?.get("plans").toString()) + val fetched = JsonUtil.fromJson>(result?.get("plans").toString()) val plans = plansMap(fetched) if (providers != null) cb.onSuccess(plans, providers) } @@ -228,10 +221,10 @@ open class LanternHttpClient : HttpClient() { ) { Logger.d(TAG, "Plans v3 Response body $result") val methods = - parseData>>( + JsonUtil.fromJson>>( result?.get("providers").toString(), ) - val icons = parseData(result?.get("icons").toString()) + val icons = JsonUtil.fromJson(result?.get("icons").toString()) Logger.d(TAG, "Plans v3 Icons Response body $icons") val providers = methods.get("android") // Due to API limitations @@ -249,7 +242,7 @@ open class LanternHttpClient : HttpClient() { } } } - val fetched = parseData>(result?.get("plans").toString()) + val fetched = JsonUtil.fromJson>(result?.get("plans").toString()) val plans = plansMap(fetched) if (providers != null) cb.onSuccess(plans, providers) } diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/PaymentsUtil.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/PaymentsUtil.kt index 24176f0d2..08d0df21f 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/util/PaymentsUtil.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/PaymentsUtil.kt @@ -363,7 +363,7 @@ class PaymentsUtil(private val activity: Activity) { ) { val currency = deviceLocal.ifEmpty { LanternApp.getSession().planByID(planID)?.let { - it.currency + it.currencyCode } ?: "usd" } Logger.d( diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/PlansUtil.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/PlansUtil.kt index 35196828a..c172cdd68 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/util/PlansUtil.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/PlansUtil.kt @@ -13,7 +13,7 @@ import org.joda.time.LocalDateTime object PlansUtil { @JvmStatic - fun updatePrice(activity: Context, plan: ProPlan) { + fun updatePrice(activity: Context, plan: ProPlan):ProPlan { val formattedBonus = formatRenewalBonusExpected(activity, plan.renewalBonusExpected, false) val totalCost = plan.costWithoutTaxStr var totalCostBilledOneTime = activity.resources.getString(R.string.total_cost, totalCost) @@ -25,13 +25,14 @@ object PlansUtil { Math.round(plan.discount * 100).toString() ) } - val oneMonthCost = plan.formattedPriceOneMonth + val oneMonthCost = plan.formattedPriceOneMonth() plan.renewalText = proRenewalText(activity.resources, formattedBonus) plan.totalCostBilledOneTime = totalCostBilledOneTime plan.oneMonthCost = oneMonthCost plan.formattedBonus = formattedBonus - plan.setFormattedDiscount(formattedDiscount) - plan.totalCost = totalCost + plan.formattedDiscount = formattedDiscount + if (totalCost != null) plan.totalCost = totalCost + return plan } private fun proRenewalText(resources: Resources, formattedBonus: String): String { diff --git a/android/build.gradle b/android/build.gradle index c2a9824e1..2c79141b6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.9.23' + ext.kotlin_version = '2.0.0' ext.signal_version = '2.8.1' ext.protoc_version = '4.26.1' ext.desugarJdk = '2.0.4' @@ -16,6 +16,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath 'com.android.tools.build:gradle:8.4.0' classpath "com.google.protobuf:protobuf-gradle-plugin:0.9.1" classpath 'com.google.gms:google-services:4.4.0' diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 509c4a29b..20db9ad5c 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/settings.gradle b/android/settings.gradle index d270bff46..52772a48f 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version '8.4.0' apply false - id "org.jetbrains.kotlin.android" version "1.9.23" apply false + id "org.jetbrains.kotlin.android" version "2.0.0" apply false } include ":app" \ No newline at end of file