cosmo.21: subscription deleted by the bot -> buy a new one by device, then replace the old subscription

This commit is contained in:
Cosmo VPN 2026-10-04 17:33:05 +03:00
parent 9e3be7a630
commit 8d8c0dafd1
6 changed files with 43 additions and 5 deletions

View file

@ -16,7 +16,7 @@ android {
minSdk = 24
targetSdk = 37
versionCode = 749
versionName = "2.3.9-cosmo.20"
versionName = "2.3.9-cosmo.21"
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
splits {

View file

@ -158,9 +158,11 @@ fun CosmoMainScreen(
}
)
offerRefresh++
// First purchase on this device: add the new subscription (import also downloads its servers).
if (newSubUrl != null && CosmoPay.subscriptionUrl() == null) onAction(MainAction.ImportBatchConfig(newSubUrl))
else onAction(MainAction.UpdateSubscriptions)
// New subscription (first purchase, or the bot deleted the expired one): drop the old one, add the new one.
if (newSubUrl != null && CosmoPay.subscriptionUrl()?.trimEnd('/') != newSubUrl.trimEnd('/')) {
CosmoPay.removeOtherSubscriptions(newSubUrl)
onAction(MainAction.ImportBatchConfig(newSubUrl))
} else onAction(MainAction.UpdateSubscriptions)
}
// Bottom bar: Home + My subscription; Settings opens from the gear (top-left) as a separate screen.

View file

@ -86,7 +86,9 @@ fun CosmoRenewSheet(onDismiss: () -> Unit, onUnfrozen: (expireAt: String?) -> Un
var mode by remember { mutableStateOf(CosmoSheetMode.Tariffs) }
var pack by remember { mutableStateOf<Int?>(null) }
// No subscription yet -> the device itself buys (the server creates the subscription after payment).
val who = remember { CosmoPay.caller(context) }
var who by remember { mutableStateOf(CosmoPay.caller(context)) }
// The bot deletes keys some time after they expire: then this device buys a new subscription.
var staleSub by remember { mutableStateOf(false) }
var reloadTick by remember { mutableIntStateOf(0) }
LaunchedEffect(reloadTick) {
@ -105,6 +107,12 @@ fun CosmoRenewSheet(onDismiss: () -> Unit, onUnfrozen: (expireAt: String?) -> Un
else -> CosmoSheetMode.Tariffs
}
} catch (e: Exception) {
if (e is CosmoPay.HttpError && e.code == 404 && who.subUrl != null) {
who = who.copy(subUrl = null)
staleSub = true
reloadTick++
return@LaunchedEffect
}
LogUtil.e(AppConfig.TAG, "CosmoPay: load offer failed", e)
error = true
}
@ -139,6 +147,14 @@ fun CosmoRenewSheet(onDismiss: () -> Unit, onUnfrozen: (expireAt: String?) -> Un
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
if (staleSub && !loading) {
Spacer(Modifier.height(4.dp))
Text(
stringResource(R.string.cosmo_renew_sub_deleted),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(Modifier.height(16.dp))
when {
loading -> Box(Modifier.fillMaxWidth().padding(24.dp), contentAlignment = Alignment.Center) { CircularProgressIndicator() }

View file

@ -3,6 +3,7 @@ package com.v2ray.ang.util
import android.content.Context
import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
@ -94,6 +95,23 @@ object CosmoPay {
null
}
/**
* Removes provider subscriptions other than [keepUrl] (e.g. the one the bot deleted after it expired),
* together with their servers. Returns how many were removed.
*/
fun removeOtherSubscriptions(keepUrl: String): Int {
val stale = try {
MmkvManager.decodeSubscriptions().filter {
val url = it.subscription.url.trim()
url.startsWith(SUB_PREFIX) && url.trimEnd('/') != keepUrl.trim().trimEnd('/')
}
} catch (_: Exception) {
emptyList()
}
stale.forEach { SettingsManager.removeSubscriptionWithDefault(it.guid) }
return stale.size
}
/** App start without a subscription: this device's existing subscription (restore) or trial availability. */
fun device(hwid: String): DeviceState {
val o = post("device", JSONObject().put("hwid", hwid))

View file

@ -152,5 +152,6 @@
<string name="cosmo_devices_done">Готово: теперь до %1$d устройств</string>
<string name="cosmo_devices_full">Все места для устройств заняты (%1$d из %2$d)</string>
<string name="cosmo_mysub_devices_used">%1$d из %2$d</string>
<string name="cosmo_renew_sub_deleted">Прежняя подписка закончилась и была удалена. Оформите новую — приложение добавит её само.</string>
</resources>

View file

@ -143,5 +143,6 @@
<string name="cosmo_devices_done">Done: now up to %1$d devices</string>
<string name="cosmo_devices_full">All device slots are in use (%1$d of %2$d)</string>
<string name="cosmo_mysub_devices_used">%1$d of %2$d</string>
<string name="cosmo_renew_sub_deleted">Your previous subscription expired and was removed. Get a new one — the app will add it automatically.</string>
</resources>