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 6fb76a6..64d4bd4 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 @@ -3,6 +3,8 @@ package com.tailscale.ipn.ui.view +import android.content.Intent +import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -10,8 +12,10 @@ 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 import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -20,13 +24,19 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +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 @@ -46,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 = { @@ -138,10 +152,47 @@ 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 @@ -171,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/res/values/strings.xml b/android/src/main/res/values/strings.xml index ed1fa16..5cefe14 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 @@ -137,6 +137,20 @@ 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