diff --git a/BusinessCard.xcodeproj/project.pbxproj b/BusinessCard.xcodeproj/project.pbxproj index ef399cd..b6fa068 100644 --- a/BusinessCard.xcodeproj/project.pbxproj +++ b/BusinessCard.xcodeproj/project.pbxproj @@ -629,6 +629,7 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; + "INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -665,6 +666,7 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; + "INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -851,6 +853,7 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; + "INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 26.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -888,6 +891,7 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; + "INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 26.0; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist b/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist index 2643284..849d755 100644 --- a/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ BusinessCard.xcscheme_^#shared#^_ orderHint - 2 + 1 BusinessCardClip.xcscheme_^#shared#^_ orderHint - 1 + 2 BusinessCardWatch Watch App.xcscheme_^#shared#^_ diff --git a/BusinessCard/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/BusinessCard/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000..70a2778 Binary files /dev/null and b/BusinessCard/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ diff --git a/BusinessCard/Assets.xcassets/AppIcon.appiconset/Contents.json b/BusinessCard/Assets.xcassets/AppIcon.appiconset/Contents.json index 2305880..ce8e776 100644 --- a/BusinessCard/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/BusinessCard/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "AppIcon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/BusinessCard/Assets.xcassets/LaunchBackground.colorset/Contents.json b/BusinessCard/Assets.xcassets/LaunchBackground.colorset/Contents.json new file mode 100644 index 0000000..d110017 --- /dev/null +++ b/BusinessCard/Assets.xcassets/LaunchBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.278", + "green" : "0.329", + "red" : "0.949" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.278", + "green" : "0.329", + "red" : "0.949" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BusinessCard/BusinessCardApp.swift b/BusinessCard/BusinessCardApp.swift index 1d31d8c..bbffc7e 100644 --- a/BusinessCard/BusinessCardApp.swift +++ b/BusinessCard/BusinessCardApp.swift @@ -72,9 +72,17 @@ struct BusinessCardApp: App { var body: some Scene { WindowGroup { - RootTabView() - .environment(appState) - .preferredColorScheme(.light) + ZStack { + // Base background matching launch screen - prevents white flash + Color.Branding.primary + .ignoresSafeArea() + + AppLaunchView(config: .businessCard) { + RootTabView() + .environment(appState) + .preferredColorScheme(.light) + } + } } .modelContainer(modelContainer) } diff --git a/BusinessCard/Design/BrandingConfig.swift b/BusinessCard/Design/BrandingConfig.swift new file mode 100644 index 0000000..1edbabb --- /dev/null +++ b/BusinessCard/Design/BrandingConfig.swift @@ -0,0 +1,63 @@ +// +// BrandingConfig.swift +// BusinessCard +// +// App-specific branding configurations for icons and launch screens. +// + +import SwiftUI +import Bedrock + +// MARK: - App Branding Colors + +extension Color { + /// BusinessCard branding colors for icon and launch screen. + enum Branding { + /// Primary gradient color (warm coral/red). + /// Must match LaunchBackground.colorset exactly to prevent flash. + static let primary = Color(red: 0.949, green: 0.329, blue: 0.278) + + /// Secondary gradient color (darker red). + static let secondary = Color(red: 0.65, green: 0.18, blue: 0.15) + + /// Accent color for icons and highlights. + static let accent = Color.white + } +} + +// MARK: - App Icon Configuration + +extension AppIconConfig { + /// BusinessCard app icon configuration. + static let businessCard = AppIconConfig( + title: "CARDS", + subtitle: nil, + iconSymbol: "person.text.rectangle", + primaryColor: Color.Branding.primary, + secondaryColor: Color.Branding.secondary, + accentColor: Color.Branding.accent + ) +} + +// MARK: - Launch Screen Configuration + +extension LaunchScreenConfig { + /// BusinessCard launch screen configuration. + static let businessCard = LaunchScreenConfig( + title: "BUSINESS CARD", + tagline: "Share Your Professional Identity", + iconSymbols: ["person.text.rectangle"], + cornerSymbol: nil, + decorativeSymbol: "circle.fill", + patternStyle: .dots, + layoutStyle: .iconAboveTitle, + primaryColor: Color.Branding.primary, + secondaryColor: Color.Branding.secondary, + accentColor: Color.Branding.accent, + titleColor: .white, + iconSize: 52, + titleSize: 32, + iconSpacing: 12, + animationDuration: 0.6 + ) +} diff --git a/BusinessCard/Design/BusinessCardTheme.swift b/BusinessCard/Design/BusinessCardTheme.swift new file mode 100644 index 0000000..2f5b043 --- /dev/null +++ b/BusinessCard/Design/BusinessCardTheme.swift @@ -0,0 +1,136 @@ +// +// BusinessCardTheme.swift +// BusinessCard +// +// App-specific theme conforming to Bedrock's color protocols. +// This light theme uses warm, professional tones. +// + +import SwiftUI +import Bedrock + +// MARK: - Surface Colors + +/// Surface colors with warm off-white tones for a professional light theme. +public enum BusinessCardSurfaceColors: SurfaceColorProvider { + /// Primary background - warm off-white base + public static let primary = Color(red: 0.97, green: 0.96, blue: 0.94) + + /// Secondary/elevated surface + public static let secondary = Color(red: 0.95, green: 0.95, blue: 0.95) + + /// Tertiary/card surface - most elevated + public static let tertiary = Color(red: 1.0, green: 1.0, blue: 1.0) + + /// Overlay background (for sheets/modals) + public static let overlay = Color(red: 0.97, green: 0.96, blue: 0.94) + + /// Card/grouped element background + public static let card = Color(red: 1.0, green: 1.0, blue: 1.0) + + /// Subtle fill for grouped content sections + public static let groupedFill = Color(red: 0.95, green: 0.94, blue: 0.92) + + /// Section fill for list sections + public static let sectionFill = Color(red: 0.93, green: 0.92, blue: 0.90) +} + +// MARK: - Text Colors + +public enum BusinessCardTextColors: TextColorProvider { + public static let primary = Color(red: 0.14, green: 0.14, blue: 0.17) + public static let secondary = Color(red: 0.32, green: 0.34, blue: 0.40) + public static let tertiary = Color(red: 0.56, green: 0.58, blue: 0.62) + public static let disabled = Color(red: 0.70, green: 0.72, blue: 0.75) + public static let placeholder = Color(red: 0.60, green: 0.62, blue: 0.66) + public static let inverse = Color(red: 0.98, green: 0.98, blue: 0.98) +} + +// MARK: - Accent Colors + +public enum BusinessCardAccentColors: AccentColorProvider { + /// Primary accent - warm red + public static let primary = Color(red: 0.95, green: 0.33, blue: 0.28) + + /// Light variant + public static let light = Color(red: 0.98, green: 0.50, blue: 0.45) + + /// Dark variant + public static let dark = Color(red: 0.75, green: 0.25, blue: 0.22) + + /// Secondary accent - ink/dark + public static let secondary = Color(red: 0.12, green: 0.12, blue: 0.14) +} + +// MARK: - Button Colors + +public enum BusinessCardButtonColors: ButtonColorProvider { + public static let primaryLight = Color(red: 0.98, green: 0.45, blue: 0.40) + public static let primaryDark = Color(red: 0.85, green: 0.28, blue: 0.24) + public static let secondary = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.subtle) + public static let destructive = Color.red.opacity(Design.Opacity.heavy) + public static let cancelText = Color(red: 0.32, green: 0.34, blue: 0.40) +} + +// MARK: - Status Colors + +public enum BusinessCardStatusColors: StatusColorProvider { + public static let success = Color(red: 0.2, green: 0.75, blue: 0.4) + public static let warning = Color(red: 0.95, green: 0.75, blue: 0.25) + public static let error = Color(red: 0.9, green: 0.3, blue: 0.3) + public static let info = Color(red: 0.3, green: 0.6, blue: 0.9) +} + +// MARK: - Border Colors + +public enum BusinessCardBorderColors: BorderColorProvider { + public static let subtle = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.subtle) + public static let standard = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.hint) + public static let emphasized = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.light) + public static let selected = BusinessCardAccentColors.primary.opacity(Design.Opacity.medium) +} + +// MARK: - Interactive Colors + +public enum BusinessCardInteractiveColors: InteractiveColorProvider { + public static let selected = BusinessCardAccentColors.primary.opacity(Design.Opacity.selection) + public static let hover = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.subtle) + public static let pressed = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.hint) + public static let focus = BusinessCardAccentColors.light +} + +// MARK: - Combined Theme + +/// BusinessCard's complete color theme. +/// Note: We use the individual color provider typealiases (AppSurface, AppThemeAccent, etc.) +/// directly in views rather than going through the theme type. +/// +/// This enum is not used directly but documents the theme structure. +/// The AppColorTheme conformance is omitted to avoid MainActor isolation conflicts. +public enum BusinessCardTheme { + public typealias Surface = BusinessCardSurfaceColors + public typealias Text = BusinessCardTextColors + public typealias Accent = BusinessCardAccentColors + public typealias Button = BusinessCardButtonColors + public typealias Status = BusinessCardStatusColors + public typealias Border = BusinessCardBorderColors + public typealias Interactive = BusinessCardInteractiveColors +} + +// MARK: - Convenience Typealiases + +/// Short typealiases for cleaner usage throughout the app. +/// These avoid conflicts with Bedrock's default typealiases by using unique names. +/// +/// Usage: +/// ```swift +/// .background(AppSurface.primary) +/// .foregroundStyle(AppThemeAccent.primary) +/// ``` +typealias AppSurface = BusinessCardSurfaceColors +typealias AppThemeText = BusinessCardTextColors +typealias AppThemeAccent = BusinessCardAccentColors +typealias AppButtonColors = BusinessCardButtonColors +typealias AppStatus = BusinessCardStatusColors +typealias AppBorder = BusinessCardBorderColors +typealias AppInteractive = BusinessCardInteractiveColors diff --git a/BusinessCard/Info.plist b/BusinessCard/Info.plist index 2ec3aff..5b7c325 100644 --- a/BusinessCard/Info.plist +++ b/BusinessCard/Info.plist @@ -12,5 +12,10 @@ $(CLOUDKIT_CONTAINER_IDENTIFIER) AppClipDomain $(APPCLIP_DOMAIN) + UILaunchScreen + + UIColorName + LaunchBackground + diff --git a/BusinessCard/Models/AppTab.swift b/BusinessCard/Models/AppTab.swift index b243a36..1794e88 100644 --- a/BusinessCard/Models/AppTab.swift +++ b/BusinessCard/Models/AppTab.swift @@ -4,6 +4,7 @@ enum AppTab: String, CaseIterable, Hashable, Identifiable { case cards case contacts case widgets + case settings var id: String { rawValue } } diff --git a/BusinessCard/Resources/Localizable.xcstrings b/BusinessCard/Resources/Localizable.xcstrings index 7f298c3..5753701 100644 --- a/BusinessCard/Resources/Localizable.xcstrings +++ b/BusinessCard/Resources/Localizable.xcstrings @@ -93,6 +93,10 @@ }, "Address" : { + }, + "App Clip (includes photo)" : { + "comment" : "A title for a section that allows users to generate an App Clip link for their card, including a photo.", + "isCommentAutoGenerated" : true }, "Apt, Suite, Unit (optional)" : { @@ -115,6 +119,10 @@ }, "Card Label" : { + }, + "Card not found" : { + "comment" : "Error message when a requested shared card is not found.", + "isCommentAutoGenerated" : true }, "Card style" : { "extractionState" : "stale", @@ -197,6 +205,10 @@ }, "Contact" : { + }, + "Could not load card: %@" : { + "comment" : "A description of an error that might occur when fetching a shared card. The argument is text describing the underlying error.", + "isCommentAutoGenerated" : true }, "Country" : { @@ -232,6 +244,10 @@ }, "Create your first card" : { + }, + "Creates a link that opens a mini-app for recipients to preview and save your card with photo." : { + "comment" : "A description of the feature that generates an App Clip link.", + "isCommentAutoGenerated" : true }, "Custom color" : { @@ -297,9 +313,24 @@ }, "Email or Username" : { + }, + "Expires in 7 days" : { + "comment" : "A notice displayed below an App Clip URL that indicates when it will expire.", + "isCommentAutoGenerated" : true + }, + "Failed to create share URL" : { + "comment" : "Error message when the share URL for a shared card cannot be created.", + "isCommentAutoGenerated" : true }, "First Name" : { + }, + "Generate App Clip Link" : { + + }, + "Generate New Link" : { + "comment" : "A button label that resets the generation of an App Clip URL.", + "isCommentAutoGenerated" : true }, "Header Layout" : { @@ -377,6 +408,10 @@ }, "Maiden Name" : { + }, + "Matt Bruce" : { + "comment" : "The name of the developer of the app.", + "isCommentAutoGenerated" : true }, "Messaging" : { @@ -782,6 +817,10 @@ }, "The default card is used for sharing and widgets." : { + }, + "This card has expired" : { + "comment" : "Error message indicating that the shared card has expired.", + "isCommentAutoGenerated" : true }, "This doesn't appear to be a business card QR code." : { @@ -791,6 +830,13 @@ }, "Title (optional)" : { + }, + "Upload failed: %@" : { + "comment" : "A description of an error that might occur when uploading a shared card. The argument is text describing the underlying error.", + "isCommentAutoGenerated" : true + }, + "Uploading card" : { + }, "URL" : { diff --git a/BusinessCard/State/SettingsState.swift b/BusinessCard/State/SettingsState.swift new file mode 100644 index 0000000..5695107 --- /dev/null +++ b/BusinessCard/State/SettingsState.swift @@ -0,0 +1,52 @@ +// +// SettingsState.swift +// BusinessCard +// +// Observable state for app settings. +// + +import Foundation +import Observation + +@Observable +@MainActor +final class SettingsState { + + // MARK: - App Info + + /// The app's display name. + var appName: String { + Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String + ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String + ?? "BusinessCard" + } + + /// The app's version string (e.g., "1.0.0"). + var appVersion: String { + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0" + } + + /// The app's build number. + var buildNumber: String { + Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1" + } + + /// Combined version and build string for display. + var versionString: String { + "Version \(appVersion) (\(buildNumber))" + } + + // MARK: - Debug Settings + + #if DEBUG + /// Debug-only: Simulates premium being unlocked for testing. + var isDebugPremiumEnabled: Bool { + get { UserDefaults.standard.bool(forKey: "debugPremiumEnabled") } + set { UserDefaults.standard.set(newValue, forKey: "debugPremiumEnabled") } + } + #endif + + // MARK: - Initialization + + init() {} +} diff --git a/BusinessCard/Views/RootTabView.swift b/BusinessCard/Views/RootTabView.swift index 930c123..86387fe 100644 --- a/BusinessCard/Views/RootTabView.swift +++ b/BusinessCard/Views/RootTabView.swift @@ -31,6 +31,10 @@ struct RootTabView: View { Tab(String.localized("Widgets"), systemImage: "square.grid.2x2", value: AppTab.widgets) { WidgetsView() } + + Tab(String.localized("Settings"), systemImage: "gearshape", value: AppTab.settings) { + SettingsView() + } } .sheet(isPresented: $showingShareSheet) { ShareCardView() diff --git a/BusinessCard/Views/SettingsView.swift b/BusinessCard/Views/SettingsView.swift new file mode 100644 index 0000000..69acc83 --- /dev/null +++ b/BusinessCard/Views/SettingsView.swift @@ -0,0 +1,150 @@ +// +// SettingsView.swift +// BusinessCard +// +// App settings screen using Bedrock components. +// + +import SwiftUI +import Bedrock + +struct SettingsView: View { + @State private var settingsState = SettingsState() + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: Design.Spacing.large) { + + // MARK: - About Section + + aboutSection + + // MARK: - Debug Section + + #if DEBUG + debugSection + #endif + + Spacer(minLength: Design.Spacing.xxxLarge) + } + .padding(.horizontal, Design.Spacing.large) + .padding(.top, Design.Spacing.medium) + } + .background(Color(.systemGroupedBackground)) + .navigationTitle(String.localized("Settings")) + .navigationBarTitleDisplayMode(.large) + } + } + + // MARK: - About Section + + private var aboutSection: some View { + VStack(alignment: .leading, spacing: Design.Spacing.small) { + SettingsSectionHeader( + title: String.localized("About"), + systemImage: "info.circle", + accentColor: AppThemeAccent.primary + ) + + SettingsCard( + backgroundColor: Color(.secondarySystemGroupedBackground), + borderColor: .clear + ) { + VStack(alignment: .leading, spacing: Design.Spacing.medium) { + // App Name + HStack { + Text(settingsState.appName) + .font(.system(size: Design.BaseFontSize.title, weight: .semibold)) + .foregroundStyle(.primary) + + Spacer() + } + + // Version + HStack { + Text(String.localized("Version")) + .font(.system(size: Design.BaseFontSize.body, weight: .medium)) + .foregroundStyle(.primary) + + Spacer() + + Text(settingsState.versionString) + .font(.system(size: Design.BaseFontSize.body, design: .monospaced)) + .foregroundStyle(.secondary) + } + + // Developer + HStack { + Text(String.localized("Developer")) + .font(.system(size: Design.BaseFontSize.body, weight: .medium)) + .foregroundStyle(.primary) + + Spacer() + + Text("Matt Bruce") + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.secondary) + } + } + } + } + } + + // MARK: - Debug Section + + #if DEBUG + private var debugSection: some View { + VStack(alignment: .leading, spacing: Design.Spacing.small) { + SettingsSectionHeader( + title: "Debug", + systemImage: "ant.fill", + accentColor: AppStatus.error + ) + + SettingsCard( + backgroundColor: Color(.secondarySystemGroupedBackground), + borderColor: .clear + ) { + VStack(spacing: Design.Spacing.medium) { + SettingsToggle( + title: "Enable Debug Premium", + subtitle: "Unlock all premium features for testing", + isOn: Binding( + get: { settingsState.isDebugPremiumEnabled }, + set: { settingsState.isDebugPremiumEnabled = $0 } + ), + accentColor: AppStatus.warning + ) + + SettingsNavigationRow( + title: "Icon Generator", + subtitle: "Generate and save app icon to Files", + backgroundColor: Color(.tertiarySystemGroupedBackground) + ) { + IconGeneratorView(config: .businessCard, appName: "BusinessCard") + } + + SettingsNavigationRow( + title: "Branding Preview", + subtitle: "Preview app icon and launch screen", + backgroundColor: Color(.tertiarySystemGroupedBackground) + ) { + BrandingPreviewView( + iconConfig: .businessCard, + launchConfig: .businessCard, + appName: "BusinessCard" + ) + } + } + } + } + } + #endif +} + +// MARK: - Preview + +#Preview { + SettingsView() +} diff --git a/BusinessCardClip/BusinessCardClip.entitlements b/BusinessCardClip/BusinessCardClip.entitlements index c058543..1d8b984 100644 --- a/BusinessCardClip/BusinessCardClip.entitlements +++ b/BusinessCardClip/BusinessCardClip.entitlements @@ -6,17 +6,9 @@ appclips:$(APPCLIP_DOMAIN) - com.apple.developer.icloud-container-identifiers - - $(CLOUDKIT_CONTAINER_IDENTIFIER) - - com.apple.developer.icloud-services - - CloudKit - com.apple.developer.parent-application-identifiers - $(TeamIdentifierPrefix)$(APP_BUNDLE_IDENTIFIER) + $(AppIdentifierPrefix)$(APP_BUNDLE_IDENTIFIER)