Skip to content

Commit 7aab785

Browse files
authored
android: add tailnet deletion dialog (#682)
Add dialog for deleting tailnet in user switcher view. Fixes tailscale/corp#31024 Signed-off-by: kari-ts <[email protected]>
1 parent b3626fc commit 7aab785

File tree

3 files changed

+108
-3
lines changed

3 files changed

+108
-3
lines changed

android/src/main/java/com/tailscale/ipn/ui/model/NetMap.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ class Netmap {
1515
var Domain: String,
1616
var UserProfiles: Map<String, Tailcfg.UserProfile>,
1717
var TKAEnabled: Boolean,
18-
var DNS: Tailcfg.DNSConfig? = null
18+
var DNS: Tailcfg.DNSConfig? = null,
19+
var AllCaps: List<String> = emptyList()
1920
) {
2021
// Keys are tailcfg.UserIDs thet get stringified
2122
// Helpers
@@ -51,5 +52,9 @@ class Netmap {
5152
UserProfiles == other.UserProfiles &&
5253
TKAEnabled == other.TKAEnabled
5354
}
55+
56+
fun hasCap(capability: String): Boolean {
57+
return AllCaps.contains(capability)
58+
}
5459
}
5560
}

android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@
33

44
package com.tailscale.ipn.ui.view
55

6+
import android.content.Intent
7+
import android.net.Uri
68
import androidx.compose.foundation.background
79
import androidx.compose.foundation.layout.Arrangement
810
import androidx.compose.foundation.layout.Column
911
import androidx.compose.foundation.layout.Row
1012
import androidx.compose.foundation.layout.fillMaxWidth
1113
import androidx.compose.foundation.layout.padding
1214
import androidx.compose.foundation.lazy.LazyColumn
15+
import androidx.compose.foundation.text.ClickableText
1316
import androidx.compose.material.icons.Icons
1417
import androidx.compose.material.icons.filled.MoreVert
18+
import androidx.compose.material3.AlertDialog
1519
import androidx.compose.material3.DropdownMenu
1620
import androidx.compose.material3.DropdownMenuItem
1721
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -20,13 +24,19 @@ import androidx.compose.material3.IconButton
2024
import androidx.compose.material3.MaterialTheme
2125
import androidx.compose.material3.Scaffold
2226
import androidx.compose.material3.Text
27+
import androidx.compose.material3.TextButton
2328
import androidx.compose.runtime.Composable
2429
import androidx.compose.runtime.collectAsState
2530
import androidx.compose.runtime.getValue
2631
import androidx.compose.runtime.mutableStateOf
2732
import androidx.compose.runtime.remember
33+
import androidx.compose.runtime.setValue
2834
import androidx.compose.ui.Modifier
35+
import androidx.compose.ui.platform.LocalContext
2936
import androidx.compose.ui.res.stringResource
37+
import androidx.compose.ui.text.SpanStyle
38+
import androidx.compose.ui.text.buildAnnotatedString
39+
import androidx.compose.ui.text.withStyle
3040
import androidx.compose.ui.tooling.preview.Preview
3141
import androidx.compose.ui.unit.dp
3242
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -46,10 +56,14 @@ data class UserSwitcherNav(
4656
@OptIn(ExperimentalMaterial3Api::class)
4757
@Composable
4858
fun UserSwitcherView(nav: UserSwitcherNav, viewModel: UserSwitcherViewModel = viewModel()) {
49-
5059
val users by viewModel.loginProfiles.collectAsState()
5160
val currentUser by viewModel.loggedInUser.collectAsState()
5261
val showHeaderMenu by viewModel.showHeaderMenu.collectAsState()
62+
var showDeleteDialog by remember { mutableStateOf(false) }
63+
val context = LocalContext.current
64+
val netmapState by viewModel.netmap.collectAsState()
65+
val capabilityIsOwner = "https://tailscale.com/cap/is-owner"
66+
val isOwner = netmapState?.hasCap(capabilityIsOwner) == true
5367

5468
Scaffold(
5569
topBar = {
@@ -138,10 +152,47 @@ fun UserSwitcherView(nav: UserSwitcherNav, viewModel: UserSwitcherViewModel = vi
138152
}
139153
})
140154
}
155+
156+
Lists.SectionDivider()
157+
Setting.Text(R.string.delete_tailnet, destructive = true) {
158+
showDeleteDialog = true
159+
}
141160
}
142161
}
143162
}
144163
}
164+
165+
if (showDeleteDialog) {
166+
AlertDialog(
167+
onDismissRequest = { showDeleteDialog = false },
168+
title = { Text(text = stringResource(R.string.delete_tailnet)) },
169+
text = {
170+
if (isOwner) {
171+
OwnerDeleteDialogText {
172+
val uri = Uri.parse("https://login.tailscale.com/admin/settings/general")
173+
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
174+
}
175+
} else {
176+
Text(stringResource(R.string.request_deletion_nonowner))
177+
}
178+
},
179+
confirmButton = {
180+
TextButton(
181+
onClick = {
182+
val intent =
183+
Intent(Intent.ACTION_VIEW, Uri.parse("https://tailscale.com/contact/support"))
184+
context.startActivity(intent)
185+
showDeleteDialog = false
186+
}) {
187+
Text(text = stringResource(R.string.contact_support))
188+
}
189+
},
190+
dismissButton = {
191+
TextButton(onClick = { showDeleteDialog = false }) {
192+
Text(text = stringResource(R.string.cancel))
193+
}
194+
})
195+
}
145196
}
146197

147198
@Composable
@@ -171,6 +222,41 @@ fun FusMenu(
171222
}
172223
}
173224

225+
@Composable
226+
fun OwnerDeleteDialogText(onSettingsClick: () -> Unit) {
227+
val part1 = stringResource(R.string.request_deletion_owner_part1)
228+
val part2a = stringResource(R.string.request_deletion_owner_part2a)
229+
val part2b = stringResource(R.string.request_deletion_owner_part2b)
230+
231+
val annotatedText = buildAnnotatedString {
232+
append(part1 + " ")
233+
234+
pushStringAnnotation(
235+
tag = "settings", annotation = "https://login.tailscale.com/admin/settings/general")
236+
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
237+
append("Settings > General")
238+
}
239+
pop()
240+
241+
append(" $part2a\n\n") // newline after "Delete tailnet."
242+
append(part2b)
243+
}
244+
245+
val context = LocalContext.current
246+
ClickableText(
247+
text = annotatedText,
248+
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
249+
onClick = { offset ->
250+
annotatedText
251+
.getStringAnnotations(tag = "settings", start = offset, end = offset)
252+
.firstOrNull()
253+
?.let { annotation ->
254+
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(annotation.item))
255+
context.startActivity(intent)
256+
}
257+
})
258+
}
259+
174260
@Composable
175261
fun MenuItem(text: String, onClick: () -> Unit) {
176262
DropdownMenuItem(

android/src/main/res/values/strings.xml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<string name="not_connected">Not connected</string>
1414
<string name="empty" translatable="false"> </string>
1515
<string name="template" translatable="false">%s</string>
16-
<string name="selected">Selected</string>
16+
<string name="selected">Selected</string>
1717
<string name="offline">Offline</string>
1818
<string name="ok">OK</string>
1919
<string name="_continue">Continue</string>
@@ -137,6 +137,20 @@
137137
<string name="custom_control_url_title">Custom control server URL</string>
138138
<string name="auth_key_input_title">Auth key</string>
139139

140+
<string name="delete_tailnet">Delete tailnet</string>
141+
<string name="contact_support">Contact support</string>
142+
<string name="request_deletion_nonowner">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.</string>
143+
<string name="request_deletion_owner_part1">
144+
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
145+
</string>
146+
<string name="request_deletion_owner_part2a">
147+
and look for “Delete tailnet”.
148+
</string>
149+
150+
<string name="request_deletion_owner_part2b">
151+
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.
152+
</string>
153+
140154
<!-- Strings for ExitNode picker -->
141155
<string name="choose_exit_node">Choose exit node</string>
142156
<string name="choose_mullvad_exit_node">Mullvad exit nodes</string>

0 commit comments

Comments
 (0)