diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/NetMap.kt b/android/src/main/java/com/tailscale/ipn/ui/model/NetMap.kt index 77322cb..f7a7a92 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/NetMap.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/NetMap.kt @@ -15,7 +15,8 @@ class Netmap { var Domain: String, var UserProfiles: Map, var TKAEnabled: Boolean, - var DNS: Tailcfg.DNSConfig? = null + var DNS: Tailcfg.DNSConfig? = null, + var AllCaps: List = emptyList() ) { // Keys are tailcfg.UserIDs thet get stringified // Helpers @@ -51,5 +52,9 @@ class Netmap { UserProfiles == other.UserProfiles && TKAEnabled == other.TKAEnabled } + + fun hasCap(capability: String): Boolean { + return AllCaps.contains(capability) + } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt index ea0ae56..8abfc94 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.ClickableText import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.AlertDialog @@ -33,6 +34,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -52,10 +56,14 @@ data class UserSwitcherNav( @OptIn(ExperimentalMaterial3Api::class) @Composable fun UserSwitcherView(nav: UserSwitcherNav, viewModel: UserSwitcherViewModel = viewModel()) { - val users by viewModel.loginProfiles.collectAsState() val currentUser by viewModel.loggedInUser.collectAsState() val showHeaderMenu by viewModel.showHeaderMenu.collectAsState() + var showDeleteDialog by remember { mutableStateOf(false) } + val context = LocalContext.current + val netmapState by viewModel.netmap.collectAsState() + val capabilityIsOwner = "https://tailscale.com/cap/is-owner" + val isOwner = netmapState?.hasCap(capabilityIsOwner) == true Scaffold( topBar = { @@ -145,10 +153,46 @@ fun UserSwitcherView(nav: UserSwitcherNav, viewModel: UserSwitcherViewModel = vi }) } + Lists.SectionDivider() + Setting.Text(R.string.delete_tailnet, destructive = true) { + showDeleteDialog = true + } } } } } + + if (showDeleteDialog) { + AlertDialog( + onDismissRequest = { showDeleteDialog = false }, + title = { Text(text = stringResource(R.string.delete_tailnet)) }, + text = { + if (isOwner) { + OwnerDeleteDialogText { + val uri = Uri.parse("https://login.tailscale.com/admin/settings/general") + context.startActivity(Intent(Intent.ACTION_VIEW, uri)) + } + } else { + Text(stringResource(R.string.request_deletion_nonowner)) + } + }, + confirmButton = { + TextButton( + onClick = { + val intent = + Intent(Intent.ACTION_VIEW, Uri.parse("https://tailscale.com/contact/support")) + context.startActivity(intent) + showDeleteDialog = false + }) { + Text(text = stringResource(R.string.contact_support)) + } + }, + dismissButton = { + TextButton(onClick = { showDeleteDialog = false }) { + Text(text = stringResource(R.string.cancel)) + } + }) + } } @Composable @@ -178,6 +222,41 @@ fun FusMenu( } } +@Composable +fun OwnerDeleteDialogText(onSettingsClick: () -> Unit) { + val part1 = stringResource(R.string.request_deletion_owner_part1) + val part2a = stringResource(R.string.request_deletion_owner_part2a) + val part2b = stringResource(R.string.request_deletion_owner_part2b) + + val annotatedText = buildAnnotatedString { + append(part1 + " ") + + pushStringAnnotation( + tag = "settings", annotation = "https://login.tailscale.com/admin/settings/general") + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append("Settings > General") + } + pop() + + append(" $part2a\n\n") // newline after "Delete tailnet." + append(part2b) + } + + val context = LocalContext.current + ClickableText( + text = annotatedText, + style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface), + onClick = { offset -> + annotatedText + .getStringAnnotations(tag = "settings", start = offset, end = offset) + .firstOrNull() + ?.let { annotation -> + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(annotation.item)) + context.startActivity(intent) + } + }) +} + @Composable fun MenuItem(text: String, onClick: () -> Unit) { DropdownMenuItem( diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt index 9634a86..edb41eb 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt @@ -23,6 +23,7 @@ import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn.State import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.notifier.Notifier +import com.tailscale.ipn.ui.util.AndroidTVUtil import com.tailscale.ipn.ui.util.PeerCategorizer import com.tailscale.ipn.ui.util.PeerSet import com.tailscale.ipn.ui.util.TimeUtil @@ -226,7 +227,7 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { } fun checkIfTaildropDirectorySelected() { - if (skipPromptsForAuthKeyLogin()) { + if (skipPromptsForAuthKeyLogin() || AndroidTVUtil.isAndroidTV()) { return } diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index d524e95..97d7edc 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -13,7 +13,7 @@ Not connected %s - Selected + Selected Offline OK Continue @@ -136,7 +136,20 @@ Invalid key Custom control server URL Auth key - + Delete tailnet + Contact support + All requests related to the removal or deletion of data are handled by our Support team. To open a request, tap the Contact Support button below to be taken to our contact form in the browser. Complete the form, and a Customer Support Engineer will work with you directly to assist. + + As the owner of this tailnet, to remove yourself from the tailnet you can either reassign ownership and contact our Support team, or delete the whole tailnet through the admin console. To do the latter, go to + + + and look for “Delete tailnet”. + + + + All requests related to the removal or deletion of data are handled by our Support team. To open a request, tap the Contact Support button below to be taken to our contact form in the browser. Complete the form, and a Customer Support Engineer will work with you directly to assist. + + Choose exit node Mullvad exit nodes diff --git a/go.sum b/go.sum index 3dc2046..11b45be 100644 --- a/go.sum +++ b/go.sum @@ -236,4 +236,4 @@ howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= tailscale.com v1.87.0-pre.0.20250801224156-0f15e4419683 h1:meEUX1Nsr5SaXiaeivOGG4c7gsQm/P3Jr3dzbtE0j6k= -tailscale.com v1.87.0-pre.0.20250801224156-0f15e4419683/go.mod h1:Lm8dnzU2i/Emw15r6sl3FRNp/liSQ/nYw6ZSQvIdZ1M= +tailscale.com v1.87.0-pre.0.20250801224156-0f15e4419683/go.mod h1:Lm8dnzU2i/Emw15r6sl3FRNp/liSQ/nYw6ZSQvIdZ1M= \ No newline at end of file