Reorganize share UI with Easy Share on top

Move Easy Share (.casera file) section above Share Code section in the
invite users dialog. Both platforms now have consistent UI with both
buttons using the same filled button style.

iOS changes:
- Move share functionality into ManageUsersView
- Remove share button from ResidenceDetailView toolbar
- Redesign ShareCodeCard with Easy Share on top

Android changes:
- Update ManageUsersDialog with matching layout
- Connect share package callback to existing share function

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-14 13:17:19 -06:00
parent e2d264da7e
commit b150c20e4b
6 changed files with 311 additions and 123 deletions

View File

@@ -5,13 +5,21 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.casera.models.ResidenceUser import com.example.casera.models.ResidenceUser
import com.example.casera.models.ResidenceShareCode import com.example.casera.models.ResidenceShareCode
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
@@ -26,7 +34,8 @@ fun ManageUsersDialog(
isPrimaryOwner: Boolean, isPrimaryOwner: Boolean,
residenceOwnerId: Int, residenceOwnerId: Int,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onUserRemoved: () -> Unit = {} onUserRemoved: () -> Unit = {},
onSharePackage: () -> Unit = {}
) { ) {
var users by remember { mutableStateOf<List<ResidenceUser>>(emptyList()) } var users by remember { mutableStateOf<List<ResidenceUser>>(emptyList()) }
val ownerId = residenceOwnerId val ownerId = residenceOwnerId
@@ -37,6 +46,7 @@ fun ManageUsersDialog(
val residenceApi = remember { ResidenceApi() } val residenceApi = remember { ResidenceApi() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current
// Load users // Load users
LaunchedEffect(residenceId) { LaunchedEffect(residenceId) {
@@ -69,7 +79,16 @@ fun ManageUsersDialog(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text("Manage Users") Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.PersonAdd,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Invite Others")
}
IconButton(onClick = onDismiss) { IconButton(onClick = onDismiss) {
Icon(Icons.Default.Close, "Close") Icon(Icons.Default.Close, "Close")
} }
@@ -91,70 +110,147 @@ fun ManageUsersDialog(
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) )
} else { } else {
// Share code section (primary owner only) // Share sections (primary owner only)
if (isPrimaryOwner) { if (isPrimaryOwner) {
// Easy Share section (on top - recommended)
Card( Card(
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer containerColor = MaterialTheme.colorScheme.surfaceVariant
) )
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Easy Share",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { onSharePackage() },
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Share, "Share", modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.width(8.dp))
Text("Send Invite Link")
}
Text(
text = "Send a .casera file via Messages, Email, or share. They just tap to join.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp)
)
}
}
// Divider with "or"
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
HorizontalDivider(modifier = Modifier.weight(1f))
Text(
text = "or",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp)
)
HorizontalDivider(modifier = Modifier.weight(1f))
}
// Share Code section
Card(
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Share Code",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column(modifier = Modifier.weight(1f)) { if (shareCode != null) {
Text( Text(
text = "Share Code", text = shareCode!!.code,
style = MaterialTheme.typography.titleSmall style = MaterialTheme.typography.headlineMedium.copy(
fontFamily = FontFamily.Monospace,
letterSpacing = 4.sp
),
color = MaterialTheme.colorScheme.primary
) )
if (shareCode != null) { IconButton(
Text( onClick = {
text = shareCode!!.code, clipboardManager.setText(AnnotatedString(shareCode!!.code))
style = MaterialTheme.typography.headlineMedium, }
color = MaterialTheme.colorScheme.primary ) {
) Icon(
} else { Icons.Default.ContentCopy,
Text( contentDescription = "Copy code",
text = "No active code", tint = MaterialTheme.colorScheme.primary
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} else {
Text(
text = "No active code",
style = MaterialTheme.typography.bodyMedium.copy(
fontStyle = FontStyle.Italic
),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} }
}
FilledTonalButton( Spacer(modifier = Modifier.height(8.dp))
onClick = {
scope.launch { Button(
isGeneratingCode = true onClick = {
val token = TokenStorage.getToken() scope.launch {
if (token != null) { isGeneratingCode = true
when (val result = residenceApi.generateShareCode(token, residenceId)) { val token = TokenStorage.getToken()
is ApiResult.Success -> { if (token != null) {
shareCode = result.data.shareCode when (val result = residenceApi.generateShareCode(token, residenceId)) {
} is ApiResult.Success -> {
is ApiResult.Error -> { shareCode = result.data.shareCode
error = result.message
}
else -> {}
} }
is ApiResult.Error -> {
error = result.message
}
else -> {}
} }
isGeneratingCode = false
} }
}, isGeneratingCode = false
enabled = !isGeneratingCode }
) { },
Icon(Icons.Default.Share, "Generate", modifier = Modifier.size(18.dp)) enabled = !isGeneratingCode,
Spacer(modifier = Modifier.width(8.dp)) modifier = Modifier.fillMaxWidth()
Text(if (shareCode != null) "New Code" else "Generate") ) {
if (isGeneratingCode) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
)
} else {
Icon(Icons.Default.Refresh, "Generate", modifier = Modifier.size(18.dp))
} }
Spacer(modifier = Modifier.width(8.dp))
Text(if (shareCode != null) "Generate New Code" else "Generate Code")
} }
if (shareCode != null) { if (shareCode != null) {
Text( Text(
text = "Share this code with others to give them access to $residenceName", text = "Share this 6-character code. They can enter it in the app to join.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp)
@@ -172,7 +268,7 @@ fun ManageUsersDialog(
) )
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxWidth().height(300.dp) modifier = Modifier.fillMaxWidth().height(200.dp)
) { ) {
items(users) { user -> items(users) { user ->
UserListItem( UserListItem(

View File

@@ -260,6 +260,10 @@ fun ResidenceDetailScreen(
residenceViewModel.getResidence(residenceId) { result -> residenceViewModel.getResidence(residenceId) { result ->
residenceState = result residenceState = result
} }
},
onSharePackage = {
// Use the existing share function with the residence
shareResidence(residence)
} }
) )
} }

View File

@@ -16942,6 +16942,10 @@
}, },
"Downloading..." : { "Downloading..." : {
},
"Easy Share" : {
"comment" : "A section header for the \"Easy Share\" feature on the Share Code Card.",
"isCommentAutoGenerated" : true
}, },
"Edit" : { "Edit" : {
"comment" : "A label for an edit action.", "comment" : "A label for an edit action.",
@@ -16985,6 +16989,10 @@
"comment" : "A description below the email input field, instructing the user to enter their email address to receive a password reset code.", "comment" : "A description below the email input field, instructing the user to enter their email address to receive a password reset code.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Error" : {
"comment" : "The title of an alert that appears when there's an error.",
"isCommentAutoGenerated" : true
},
"error_generic" : { "error_generic" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -17330,8 +17338,12 @@
"comment" : "A label indicating a free feature.", "comment" : "A label indicating a free feature.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Generate" : { "Generate Code" : {
"comment" : "A button label that says \"Generate\".", "comment" : "A button label that generates a new invitation code.",
"isCommentAutoGenerated" : true
},
"Generate New Code" : {
"comment" : "A button label that appears when a user wants to generate a new invitation code.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Hour" : { "Hour" : {
@@ -17370,6 +17382,10 @@
"comment" : "A label displayed next to an image of a play button, indicating that a task is currently in progress.", "comment" : "A label displayed next to an image of a play button, indicating that a task is currently in progress.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Invite Others" : {
"comment" : "A header that suggests inviting others to join the app.",
"isCommentAutoGenerated" : true
},
"Join a Residence" : { "Join a Residence" : {
"comment" : "A button label that instructs the user to join an existing residence.", "comment" : "A button label that instructs the user to join an existing residence.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@@ -17419,10 +17435,6 @@
}, },
"Need inspiration?" : { "Need inspiration?" : {
},
"New Code" : {
"comment" : "A button label that appears when the user has already generated a share code.",
"isCommentAutoGenerated" : true
}, },
"New Password" : { "New Password" : {
@@ -24722,6 +24734,13 @@
"comment" : "A label displayed above the picker for selecting the notification time.", "comment" : "A label displayed above the picker for selecting the notification time.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Send a .casera file via Messages, Email, or AirDrop. They just tap to join." : {
},
"Send Invite Link" : {
"comment" : "A button label for sending an invitation link to others.",
"isCommentAutoGenerated" : true
},
"Send New Code" : { "Send New Code" : {
"comment" : "A button label that allows a user to request a new verification code.", "comment" : "A button label that allows a user to request a new verification code.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@@ -24871,12 +24890,8 @@
"comment" : "A label displayed above the share code section of the view.", "comment" : "A label displayed above the share code section of the view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Share Failed" : { "Share this 6-character code. They can enter it in the app to join." : {
"comment" : "An alert title that appears when sharing a file fails.", "comment" : "A description of how to share the invitation code with others.",
"isCommentAutoGenerated" : true
},
"Share this code with others to give them access to %@" : {
"comment" : "A caption below the share code, explaining that it can be shared with others to give them access to a residence. The argument is the name of the residence.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Shared Users (%lld)" : { "Shared Users (%lld)" : {

View File

@@ -6,6 +6,7 @@ struct ManageUsersView: View {
let residenceName: String let residenceName: String
let isPrimaryOwner: Bool let isPrimaryOwner: Bool
let residenceOwnerId: Int32 let residenceOwnerId: Int32
let residence: ResidenceResponse?
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var users: [ResidenceUserResponse] = [] @State private var users: [ResidenceUserResponse] = []
@@ -14,6 +15,8 @@ struct ManageUsersView: View {
@State private var isLoading = true @State private var isLoading = true
@State private var errorMessage: String? @State private var errorMessage: String?
@State private var isGeneratingCode = false @State private var isGeneratingCode = false
@State private var shareFileURL: URL?
@StateObject private var sharingManager = ResidenceSharingManager.shared
var body: some View { var body: some View {
NavigationView { NavigationView {
@@ -36,7 +39,9 @@ struct ManageUsersView: View {
shareCode: shareCode, shareCode: shareCode,
residenceName: residenceName, residenceName: residenceName,
isGeneratingCode: isGeneratingCode, isGeneratingCode: isGeneratingCode,
onGenerateCode: generateShareCode isGeneratingPackage: sharingManager.isGeneratingPackage,
onGenerateCode: generateShareCode,
onEasyShare: easyShare
) )
.padding(.horizontal) .padding(.horizontal)
.padding(.top) .padding(.top)
@@ -82,6 +87,32 @@ struct ManageUsersView: View {
shareCode = nil shareCode = nil
loadUsers() loadUsers()
} }
.sheet(isPresented: Binding(
get: { shareFileURL != nil },
set: { if !$0 { shareFileURL = nil } }
)) {
if let url = shareFileURL {
ShareSheet(activityItems: [url])
}
}
.alert("Error", isPresented: Binding(
get: { sharingManager.errorMessage != nil },
set: { if !$0 { sharingManager.errorMessage = nil } }
)) {
Button("OK", role: .cancel) {}
} message: {
Text(sharingManager.errorMessage ?? "Failed to create share link.")
}
}
private func easyShare() {
guard let residence = residence else { return }
Task {
if let url = await sharingManager.createShareableFile(residence: residence) {
shareFileURL = url
}
}
} }
private func loadUsers() { private func loadUsers() {
@@ -196,5 +227,5 @@ struct ManageUsersView: View {
} }
#Preview { #Preview {
ManageUsersView(residenceId: 1, residenceName: "My Home", isPrimaryOwner: true, residenceOwnerId: 1) ManageUsersView(residenceId: 1, residenceName: "My Home", isPrimaryOwner: true, residenceOwnerId: 1, residence: nil)
} }

View File

@@ -33,10 +33,7 @@ struct ResidenceDetailView: View {
@State private var isDeleting = false @State private var isDeleting = false
@State private var showingUpgradePrompt = false @State private var showingUpgradePrompt = false
@State private var upgradeTriggerKey = "" @State private var upgradeTriggerKey = ""
@State private var showShareSheet = false
@State private var shareFileURL: URL?
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared @StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
@StateObject private var sharingManager = ResidenceSharingManager.shared
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@@ -125,7 +122,8 @@ struct ResidenceDetailView: View {
residenceId: residence.id, residenceId: residence.id,
residenceName: residence.name, residenceName: residence.name,
isPrimaryOwner: isCurrentUserOwner(of: residence), isPrimaryOwner: isCurrentUserOwner(of: residence),
residenceOwnerId: residence.ownerId residenceOwnerId: residence.ownerId,
residence: residence
) )
} }
} }
@@ -150,22 +148,6 @@ struct ResidenceDetailView: View {
.sheet(isPresented: $showingUpgradePrompt) { .sheet(isPresented: $showingUpgradePrompt) {
UpgradePromptView(triggerKey: upgradeTriggerKey.isEmpty ? "add_11th_task" : upgradeTriggerKey, isPresented: $showingUpgradePrompt) UpgradePromptView(triggerKey: upgradeTriggerKey.isEmpty ? "add_11th_task" : upgradeTriggerKey, isPresented: $showingUpgradePrompt)
} }
.sheet(isPresented: $showShareSheet) {
if let url = shareFileURL {
ShareSheet(activityItems: [url])
}
}
// Share error alert
.alert("Share Failed", isPresented: .init(
get: { sharingManager.errorMessage != nil },
set: { if !$0 { sharingManager.resetState() } }
)) {
Button("OK") {
sharingManager.resetState()
}
} message: {
Text(sharingManager.errorMessage ?? "Failed to create share link.")
}
// MARK: onChange & lifecycle // MARK: onChange & lifecycle
.onChange(of: viewModel.reportMessage) { message in .onChange(of: viewModel.reportMessage) { message in
@@ -357,26 +339,7 @@ private extension ResidenceDetailView {
.disabled(viewModel.isGeneratingReport) .disabled(viewModel.isGeneratingReport)
} }
// Share Residence button (owner only) // Manage Users button (owner only) - includes share code generation and easy share
if let residence = viewModel.selectedResidence, isCurrentUserOwner(of: residence) {
Button {
if subscriptionCache.canShareResidence() {
shareResidence(residence)
} else {
upgradeTriggerKey = "share_residence"
showingUpgradePrompt = true
}
} label: {
if sharingManager.isGeneratingPackage {
ProgressView()
} else {
Image(systemName: "square.and.arrow.up")
}
}
.disabled(sharingManager.isGeneratingPackage)
}
// Manage Users button (owner only)
if let residence = viewModel.selectedResidence, isCurrentUserOwner(of: residence) { if let residence = viewModel.selectedResidence, isCurrentUserOwner(of: residence) {
Button { Button {
if subscriptionCache.canShareResidence() { if subscriptionCache.canShareResidence() {
@@ -415,15 +378,6 @@ private extension ResidenceDetailView {
} }
} }
} }
func shareResidence(_ residence: ResidenceResponse) {
Task {
if let url = await sharingManager.createShareableFile(residence: residence) {
shareFileURL = url
showShareSheet = true
}
}
}
} }
// MARK: - Data Loading // MARK: - Data Loading

View File

@@ -6,48 +6,136 @@ struct ShareCodeCard: View {
let shareCode: ShareCodeResponse? let shareCode: ShareCodeResponse?
let residenceName: String let residenceName: String
let isGeneratingCode: Bool let isGeneratingCode: Bool
let isGeneratingPackage: Bool
let onGenerateCode: () -> Void let onGenerateCode: () -> Void
let onEasyShare: () -> Void
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 16) {
// Header
HStack { HStack {
VStack(alignment: .leading, spacing: 4) { Image(systemName: "person.badge.plus")
Text("Share Code") .font(.title2)
.font(.subheadline) .foregroundColor(Color.appPrimary)
.foregroundColor(Color.appTextSecondary) Text("Invite Others")
.font(.headline)
.foregroundColor(Color.appTextPrimary)
}
// Easy Share Section (on top - recommended method)
VStack(alignment: .leading, spacing: 8) {
Text("Easy Share")
.font(.subheadline)
.foregroundColor(Color.appTextSecondary)
Button(action: onEasyShare) {
HStack {
if isGeneratingPackage {
ProgressView()
.tint(.white)
} else {
Image(systemName: "square.and.arrow.up")
}
Text("Send Invite Link")
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.disabled(isGeneratingPackage)
Text("Send a .casera file via Messages, Email, or AirDrop. They just tap to join.")
.font(.caption)
.foregroundColor(Color.appTextSecondary)
}
.padding()
.background(Color.appBackgroundSecondary)
.cornerRadius(12)
// Divider with "or"
HStack {
Rectangle()
.fill(Color.appTextSecondary.opacity(0.3))
.frame(height: 1)
Text("or")
.font(.caption)
.foregroundColor(Color.appTextSecondary)
Rectangle()
.fill(Color.appTextSecondary.opacity(0.3))
.frame(height: 1)
}
// Share Code Section
VStack(alignment: .leading, spacing: 8) {
Text("Share Code")
.font(.subheadline)
.foregroundColor(Color.appTextSecondary)
HStack {
if let shareCode = shareCode { if let shareCode = shareCode {
Text(shareCode.code) Text(shareCode.code)
.font(.title) .font(.system(size: 32, weight: .bold, design: .monospaced))
.fontWeight(.bold)
.foregroundColor(Color.appPrimary) .foregroundColor(Color.appPrimary)
.kerning(4)
Spacer()
Button {
UIPasteboard.general.string = shareCode.code
} label: {
Image(systemName: "doc.on.doc")
.foregroundColor(Color.appPrimary)
}
.buttonStyle(.bordered)
} else { } else {
Text("No active code") Text("No active code")
.font(.body) .font(.body)
.foregroundColor(Color.appTextSecondary) .foregroundColor(Color.appTextSecondary)
.italic()
Spacer()
} }
} }
Spacer() // Generate Code Button
Button(action: onGenerateCode) { Button(action: onGenerateCode) {
HStack { HStack {
Image(systemName: "square.and.arrow.up") if isGeneratingCode {
Text(shareCode != nil ? "New Code" : "Generate") ProgressView()
.tint(.white)
} else {
Image(systemName: "arrow.clockwise")
}
Text(shareCode != nil ? "Generate New Code" : "Generate Code")
} }
.frame(maxWidth: .infinity)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.disabled(isGeneratingCode) .disabled(isGeneratingCode)
}
if shareCode != nil { if shareCode != nil {
Text("Share this code with others to give them access to \(residenceName)") Text("Share this 6-character code. They can enter it in the app to join.")
.font(.caption) .font(.caption)
.foregroundColor(Color.appTextSecondary) .foregroundColor(Color.appTextSecondary)
}
} }
.padding()
.background(Color.appBackgroundSecondary)
.cornerRadius(12)
} }
.padding() .padding()
.background(Color.appPrimary.opacity(0.1)) .background(Color.appPrimary.opacity(0.05))
.cornerRadius(12) .cornerRadius(16)
} }
} }
#Preview {
ShareCodeCard(
shareCode: nil,
residenceName: "My Home",
isGeneratingCode: false,
isGeneratingPackage: false,
onGenerateCode: {},
onEasyShare: {}
)
.padding()
}