diff --git a/GitStreak.xcodeproj/project.pbxproj b/GitStreak.xcodeproj/project.pbxproj index 549dfec..64dfe8c 100644 --- a/GitStreak.xcodeproj/project.pbxproj +++ b/GitStreak.xcodeproj/project.pbxproj @@ -9,6 +9,10 @@ /* Begin PBXBuildFile section */ 1A2B3C4D5E6F7G8H9I0J1K2L /* GitStreakApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D5E6F7G8H9I0J1K2M /* GitStreakApp.swift */; }; 1A2B3C4D5E6F7G8H9I0J1K2N /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D5E6F7G8H9I0J1K2O /* ContentView.swift */; }; + 2F37473F4A3E447A94D5BD80 /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29553B8F01694BF2B7DC3039 /* AppConstants.swift */; }; + 7B17ACAC872B4E6882454524 /* PrivacyPolicyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BBE44C1AF9E4EDFB678D375 /* PrivacyPolicyView.swift */; }; + 7DAD5D8097AD443E8574373B /* TermsOfServiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A992FC569EA64D0AB98A86A8 /* TermsOfServiceView.swift */; }; + 37824FFD043A4C92A86BE006 /* HelpSupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDEDA15ABB534688910091EF /* HelpSupportView.swift */; }; 1A2B3C4D5E6F7G8H9I0J1K2P /* GitStreakData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D5E6F7G8H9I0J1K2Q /* GitStreakData.swift */; }; 1A2B3C4D5E6F7G8H9I0J1K2R /* StreakCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D5E6F7G8H9I0J1K2S /* StreakCardView.swift */; }; 1A2B3C4D5E6F7G8H9I0J1K2T /* LevelProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D5E6F7G8H9I0J1K2U /* LevelProgressView.swift */; }; @@ -34,6 +38,10 @@ /* Begin PBXFileReference section */ 1A2B3C4D5E6F7G8H9I0J1K2M /* GitStreakApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitStreakApp.swift; sourceTree = ""; }; 1A2B3C4D5E6F7G8H9I0J1K2O /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 29553B8F01694BF2B7DC3039 /* AppConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = ""; }; + 2BBE44C1AF9E4EDFB678D375 /* PrivacyPolicyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyView.swift; sourceTree = ""; }; + A992FC569EA64D0AB98A86A8 /* TermsOfServiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceView.swift; sourceTree = ""; }; + FDEDA15ABB534688910091EF /* HelpSupportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpSupportView.swift; sourceTree = ""; }; 1A2B3C4D5E6F7G8H9I0J1K2Q /* GitStreakData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitStreakData.swift; sourceTree = ""; }; 1A2B3C4D5E6F7G8H9I0J1K2S /* StreakCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreakCardView.swift; sourceTree = ""; }; 1A2B3C4D5E6F7G8H9I0J1K2U /* LevelProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LevelProgressView.swift; sourceTree = ""; }; @@ -84,6 +92,7 @@ children = ( 1A2B3C4D5E6F7G8H9I0J1K2M /* GitStreakApp.swift */, 1A2B3C4D5E6F7G8H9I0J1K2O /* ContentView.swift */, + 29553B8F01694BF2B7DC3039 /* AppConstants.swift */, 1A2B3C4D5E6F7G8H9I0J1K42 /* Models */, 1A2B3C4D5E6F7G8H9I0J1K43 /* Views */, 1A2B3C4D5E6F7G8H9I0J1K34 /* Assets.xcassets */, @@ -119,6 +128,9 @@ 1A2B3C4D5E6F7G8H9I0J1K30 /* AchievementsView.swift */, 1A2B3C4D5E6F7G8H9I0J1K3B /* AllCommitsView.swift */, 1A2B3C4D5E6F7G8H9I0J1K32 /* TabBarView.swift */, + 2BBE44C1AF9E4EDFB678D375 /* PrivacyPolicyView.swift */, + A992FC569EA64D0AB98A86A8 /* TermsOfServiceView.swift */, + FDEDA15ABB534688910091EF /* HelpSupportView.swift */, ); path = Views; sourceTree = ""; @@ -237,6 +249,10 @@ buildActionMask = 2147483647; files = ( 1A2B3C4D5E6F7G8H9I0J1K2N /* ContentView.swift in Sources */, + 2F37473F4A3E447A94D5BD80 /* AppConstants.swift in Sources */, + 7B17ACAC872B4E6882454524 /* PrivacyPolicyView.swift in Sources */, + 7DAD5D8097AD443E8574373B /* TermsOfServiceView.swift in Sources */, + 37824FFD043A4C92A86BE006 /* HelpSupportView.swift in Sources */, 1A2B3C4D5E6F7G8H9I0J1K2P /* GitStreakData.swift in Sources */, 1A2B3C4D5E6F7G8H9I0J1K2R /* StreakCardView.swift in Sources */, 1A2B3C4D5E6F7G8H9I0J1K2T /* LevelProgressView.swift in Sources */, @@ -392,7 +408,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"GitStreak/Preview Content\""; DEVELOPMENT_TEAM = 389Y5DY6GV; ENABLE_PREVIEWS = YES; @@ -420,7 +436,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"GitStreak/Preview Content\""; DEVELOPMENT_TEAM = 389Y5DY6GV; ENABLE_PREVIEWS = YES; @@ -447,7 +463,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 389Y5DY6GV; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.0; @@ -466,7 +482,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 389Y5DY6GV; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.0; diff --git a/GitStreak/AppConstants.swift b/GitStreak/AppConstants.swift new file mode 100644 index 0000000..49cecfa --- /dev/null +++ b/GitStreak/AppConstants.swift @@ -0,0 +1,23 @@ +import Foundation + +struct AppConstants { + // MARK: - Legal Document Dates + static let privacyPolicyLastUpdated = "August 27, 2025" + static let termsOfServiceEffectiveDate = "August 27, 2025" + + // MARK: - Contact Information + static let supportEmail = "gitstreakapp@gmail.com" + + // MARK: - External URLs + static let githubIssuesURL = "https://github.com/popand/gitstreak/issues" + static let githubTokenGenerationURL = "https://github.com/settings/tokens/new?scopes=repo,user&description=GitStreak%20App" + + // MARK: - App Information + static var appVersion: String { + return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" + } + + static var buildNumber: String { + return Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" + } +} \ No newline at end of file diff --git a/GitStreak/ContentView.swift b/GitStreak/ContentView.swift index 996d66c..ce216ec 100644 --- a/GitStreak/ContentView.swift +++ b/GitStreak/ContentView.swift @@ -293,6 +293,9 @@ struct SettingsView: View { @State private var isAuthenticating = false @State private var showError = false @State private var errorMessage = "" + @State private var showingPrivacyPolicy = false + @State private var showingTermsOfService = false + @State private var showingHelpSupport = false var body: some View { NavigationView { @@ -307,6 +310,91 @@ struct SettingsView: View { } else { authenticationView } + + // Legal & Support Section + VStack(spacing: 12) { + Text("Resources") + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + + VStack(spacing: 0) { + // Help & Support + Button(action: { showingHelpSupport = true }) { + HStack { + Image(systemName: "questionmark.circle.fill") + .font(.system(size: 20)) + .foregroundColor(.blue) + .frame(width: 28) + + Text("Help & Support") + .font(.system(size: 14)) + .foregroundColor(.primary) + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.secondary.opacity(0.5)) + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + } + + Divider() + .padding(.leading, 60) + + // Privacy Policy + Button(action: { showingPrivacyPolicy = true }) { + HStack { + Image(systemName: "lock.shield.fill") + .font(.system(size: 20)) + .foregroundColor(.green) + .frame(width: 28) + + Text("Privacy Policy") + .font(.system(size: 14)) + .foregroundColor(.primary) + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.secondary.opacity(0.5)) + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + } + + Divider() + .padding(.leading, 60) + + // Terms of Service + Button(action: { showingTermsOfService = true }) { + HStack { + Image(systemName: "doc.text.fill") + .font(.system(size: 20)) + .foregroundColor(.purple) + .frame(width: 28) + + Text("Terms of Service") + .font(.system(size: 14)) + .foregroundColor(.primary) + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.secondary.opacity(0.5)) + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + } + } + .background(Color(.systemBackground)) + .cornerRadius(12) + } + .padding(.top, 8) } .padding(.horizontal, 20) .padding(.top, 16) @@ -317,11 +405,11 @@ struct SettingsView: View { .navigationBarTitleDisplayMode(.large) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { dismiss() }) { - Text("Done") - .fontWeight(.semibold) - .foregroundColor(.blue) + Button("Done") { + dismiss() } + .fontWeight(.semibold) + .foregroundColor(.blue) } } } @@ -330,6 +418,15 @@ struct SettingsView: View { } message: { Text(errorMessage) } + .sheet(isPresented: $showingPrivacyPolicy) { + PrivacyPolicyView() + } + .sheet(isPresented: $showingTermsOfService) { + TermsOfServiceView() + } + .sheet(isPresented: $showingHelpSupport) { + HelpSupportView() + } } private var authenticatedView: some View { @@ -593,7 +690,7 @@ struct SettingsView: View { } Button(action: { - if let url = URL(string: "https://github.com/settings/tokens/new?scopes=repo,user&description=GitStreak%20App") { + if let url = URL(string: AppConstants.githubTokenGenerationURL) { UIApplication.shared.open(url) } }) { diff --git a/GitStreak/Views/HelpSupportView.swift b/GitStreak/Views/HelpSupportView.swift new file mode 100644 index 0000000..9d27d04 --- /dev/null +++ b/GitStreak/Views/HelpSupportView.swift @@ -0,0 +1,370 @@ +import SwiftUI +import MessageUI + +struct HelpSupportView: View { + @Environment(\.dismiss) var dismiss + @State private var showingMailComposer = false + @State private var expandedSection: String? = nil + + var body: some View { + NavigationView { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + // Getting Started Section + Section { + VStack(alignment: .leading, spacing: 16) { + Text("Getting Started") + .font(.title2) + .fontWeight(.bold) + + VStack(alignment: .leading, spacing: 12) { + HelpItem( + icon: "1.circle.fill", + title: "Connect Your GitHub Account", + description: "Tap the gear icon and enter your Personal Access Token to connect your GitHub account." + ) + + HelpItem( + icon: "2.circle.fill", + title: "Generate a Personal Access Token", + description: "Visit GitHub Settings → Developer settings → Personal access tokens → Generate new token with 'repo' and 'user' scopes." + ) + + HelpItem( + icon: "3.circle.fill", + title: "View Your Stats", + description: "Once connected, your commit streaks, achievements, and activity will automatically sync." + ) + } + } + .padding(.vertical, 8) + } + + Divider() + + // Frequently Asked Questions + Section { + VStack(alignment: .leading, spacing: 16) { + Text("Frequently Asked Questions") + .font(.title2) + .fontWeight(.bold) + + FAQItem( + question: "Why isn't my data updating?", + answer: "Pull down to refresh on any screen to sync with GitHub. Make sure your token has the correct permissions (repo and user scopes).", + isExpanded: expandedSection == "update", + onTap: { toggleSection("update") } + ) + + FAQItem( + question: "How are streaks calculated?", + answer: "Streaks count consecutive days with at least one commit. The current streak shows days from your last commit, and the best streak is your longest ever.", + isExpanded: expandedSection == "streaks", + onTap: { toggleSection("streaks") } + ) + + FAQItem( + question: "Is my GitHub token secure?", + answer: "Yes! Your token is stored securely in the iOS Keychain and is never shared with third parties. It's only used for direct GitHub API requests.", + isExpanded: expandedSection == "security", + onTap: { toggleSection("security") } + ) + + FAQItem( + question: "Can I use multiple GitHub accounts?", + answer: "Currently, GitStreak supports one GitHub account at a time. You can disconnect and reconnect with a different account in Settings.", + isExpanded: expandedSection == "accounts", + onTap: { toggleSection("accounts") } + ) + + FAQItem( + question: "What data is tracked?", + answer: "We track your public commits, repositories, contribution statistics, and calculate streaks. All data comes directly from GitHub's public API.", + isExpanded: expandedSection == "data", + onTap: { toggleSection("data") } + ) + } + .padding(.vertical, 8) + } + + Divider() + + // Troubleshooting + Section { + VStack(alignment: .leading, spacing: 16) { + Text("Troubleshooting") + .font(.title2) + .fontWeight(.bold) + + TroubleshootItem( + issue: "Authentication Failed", + solution: "Ensure your token is valid and has 'repo' and 'user' scopes. Try generating a new token if the issue persists." + ) + + TroubleshootItem( + issue: "Missing Commits", + solution: "GitStreak shows commits from public repositories and private repos your token has access to. Check your token permissions." + ) + + TroubleshootItem( + issue: "Incorrect Statistics", + solution: "Pull to refresh to sync latest data. Note that GitHub's API may have delays in reporting recent activity." + ) + } + .padding(.vertical, 8) + } + + Divider() + + // Contact Support + Section { + VStack(alignment: .leading, spacing: 16) { + Text("Contact Support") + .font(.title2) + .fontWeight(.bold) + + Text("Still need help? We're here to assist you!") + .font(.body) + .foregroundColor(.secondary) + + VStack(spacing: 12) { + // Email Support + Button(action: { + if MFMailComposeViewController.canSendMail() { + showingMailComposer = true + } else { + // Fallback to mailto URL + if let url = URL(string: "mailto:\(AppConstants.supportEmail)") { + UIApplication.shared.open(url) + } + } + }) { + HStack { + Image(systemName: "envelope.fill") + .foregroundColor(.white) + .frame(width: 24) + Text("Email Support") + .fontWeight(.medium) + Spacer() + Image(systemName: "arrow.up.right") + .font(.caption) + } + .foregroundColor(.white) + .padding() + .background(Color.blue) + .cornerRadius(12) + } + + // GitHub Issues + Button(action: { + if let url = URL(string: AppConstants.githubIssuesURL) { + UIApplication.shared.open(url) + } + }) { + HStack { + Image(systemName: "exclamationmark.bubble.fill") + .foregroundColor(.white) + .frame(width: 24) + Text("Report an Issue on GitHub") + .fontWeight(.medium) + Spacer() + Image(systemName: "arrow.up.right") + .font(.caption) + } + .foregroundColor(.white) + .padding() + .background(Color.purple) + .cornerRadius(12) + } + } + } + .padding(.vertical, 8) + } + + // App Version Info + VStack(alignment: .center, spacing: 8) { + Text("GitStreak v\(AppConstants.appVersion) (\(AppConstants.buildNumber))") + .font(.caption) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity) + .padding(.top, 20) + } + .padding(24) + } + .background(Color(.systemGroupedBackground)) + .navigationTitle("Help & Support") + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Done") { + dismiss() + } + } + } + } + .sheet(isPresented: $showingMailComposer) { + MailComposerView( + recipients: [AppConstants.supportEmail], + subject: "GitStreak Support Request", + messageBody: generateSupportEmailBody() + ) + } + } + + private func toggleSection(_ section: String) { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + if expandedSection == section { + expandedSection = nil + } else { + expandedSection = section + } + } + } + + private func generateSupportEmailBody() -> String { + let version = AppConstants.appVersion + let build = AppConstants.buildNumber + let device = UIDevice.current.model + let osVersion = UIDevice.current.systemVersion + + return """ + + + --- + Please describe your issue above this line + --- + + App Version: \(version) (\(build)) + Device: \(device) + iOS Version: \(osVersion) + """ + } +} + +struct HelpItem: View { + let icon: String + let title: String + let description: String + + var body: some View { + HStack(alignment: .top, spacing: 12) { + Image(systemName: icon) + .font(.title2) + .foregroundColor(.blue) + .frame(width: 32) + + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.headline) + Text(description) + .font(.body) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + } +} + +struct FAQItem: View { + let question: String + let answer: String + let isExpanded: Bool + let onTap: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Button(action: onTap) { + HStack { + Text(question) + .font(.headline) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) + Spacer() + Image(systemName: isExpanded ? "chevron.up" : "chevron.down") + .font(.caption) + .foregroundColor(.secondary) + } + } + + if isExpanded { + Text(answer) + .font(.body) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 4) + .transition(.opacity.combined(with: .move(edge: .top))) + } + } + .padding(.vertical, 8) + .padding(.horizontal, 16) + .background(Color(.secondarySystemGroupedBackground)) + .cornerRadius(12) + } +} + +struct TroubleshootItem: View { + let issue: String + let solution: String + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .top, spacing: 8) { + Image(systemName: "wrench.and.screwdriver.fill") + .foregroundColor(.orange) + .font(.body) + + VStack(alignment: .leading, spacing: 4) { + Text(issue) + .font(.headline) + Text(solution) + .font(.body) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + } + .padding() + .background(Color(.secondarySystemGroupedBackground)) + .cornerRadius(12) + } +} + +// Mail Composer View for iOS +struct MailComposerView: UIViewControllerRepresentable { + let recipients: [String] + let subject: String + let messageBody: String + @Environment(\.dismiss) var dismiss + + func makeUIViewController(context: Context) -> MFMailComposeViewController { + let mailComposer = MFMailComposeViewController() + mailComposer.mailComposeDelegate = context.coordinator + mailComposer.setToRecipients(recipients) + mailComposer.setSubject(subject) + mailComposer.setMessageBody(messageBody, isHTML: false) + return mailComposer + } + + func updateUIViewController(_ uiViewController: MFMailComposeViewController, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, MFMailComposeViewControllerDelegate { + var parent: MailComposerView + + init(_ parent: MailComposerView) { + self.parent = parent + } + + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + parent.dismiss() + } + } +} + +#Preview { + HelpSupportView() +} \ No newline at end of file diff --git a/GitStreak/Views/PrivacyPolicyView.swift b/GitStreak/Views/PrivacyPolicyView.swift new file mode 100644 index 0000000..ed7efff --- /dev/null +++ b/GitStreak/Views/PrivacyPolicyView.swift @@ -0,0 +1,118 @@ +import SwiftUI + +struct PrivacyPolicyView: View { + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationView { + ScrollView { + VStack(alignment: .leading, spacing: 24) { + Group { + Section { + Text("Last Updated: \(AppConstants.privacyPolicyLastUpdated)") + .font(.caption) + .foregroundColor(.secondary) + + Text("GitStreak (\"we\", \"our\", or \"us\") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, and safeguard your information when you use our iOS application.") + .font(.body) + } + + Section { + Text("Information We Collect") + .font(.headline) + + Text("**GitHub Personal Access Token**") + .font(.subheadline) + .padding(.top, 8) + + Text("• We collect and securely store your GitHub Personal Access Token in the iOS Keychain\n• This token is used solely to authenticate with GitHub's API\n• The token never leaves your device except for authorized GitHub API requests\n• We never transmit your token to our servers or any third parties") + .font(.body) + .padding(.leading, 16) + + Text("**GitHub Activity Data**") + .font(.subheadline) + .padding(.top, 8) + + Text("• We fetch your public GitHub activity including commits, repositories, and contribution statistics\n• This data is retrieved directly from GitHub's API\n• All data is processed and stored locally on your device\n• We do not collect or store this data on external servers") + .font(.body) + .padding(.leading, 16) + } + + Section { + Text("How We Use Your Information") + .font(.headline) + + Text("Your information is used exclusively to:\n• Display your GitHub contribution statistics\n• Calculate your commit streaks and achievements\n• Show your recent activity and repository information\n• Provide personalized insights about your coding habits") + .font(.body) + } + + Section { + Text("Data Storage and Security") + .font(.headline) + + Text("• All sensitive data (tokens) are stored in the iOS Keychain with encryption\n• GitHub activity data is cached locally on your device\n• We implement industry-standard security measures\n• We do not operate external servers or databases that store your data") + .font(.body) + } + } + + Group { + Section { + Text("Data Sharing") + .font(.headline) + + Text("We do not sell, trade, or otherwise transfer your personal information to third parties. Your GitHub token and activity data remain exclusively on your device and are only used for direct communication with GitHub's API.") + .font(.body) + } + + Section { + Text("Your Rights") + .font(.headline) + + Text("You have the right to:\n• Disconnect your GitHub account at any time\n• Delete all stored data by removing the app\n• Request information about data we've stored\n• Control what GitHub data is accessible via token scopes") + .font(.body) + } + + Section { + Text("Children's Privacy") + .font(.headline) + + Text("Our service is not directed to individuals under the age of 13. We do not knowingly collect personal information from children under 13.") + .font(.body) + } + + Section { + Text("Changes to This Policy") + .font(.headline) + + Text("We may update our Privacy Policy from time to time. We will notify you of any changes by updating the \"Last Updated\" date at the top of this policy.") + .font(.body) + } + + Section { + Text("Contact Us") + .font(.headline) + + Text("If you have questions about this Privacy Policy, please contact us through the Help & Support section in the app.") + .font(.body) + } + } + } + .padding(24) + } + .background(Color(.systemGroupedBackground)) + .navigationTitle("Privacy Policy") + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Done") { + dismiss() + } + } + } + } + } +} + +#Preview { + PrivacyPolicyView() +} \ No newline at end of file diff --git a/GitStreak/Views/TermsOfServiceView.swift b/GitStreak/Views/TermsOfServiceView.swift new file mode 100644 index 0000000..860ed6d --- /dev/null +++ b/GitStreak/Views/TermsOfServiceView.swift @@ -0,0 +1,149 @@ +import SwiftUI + +struct TermsOfServiceView: View { + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationView { + ScrollView { + VStack(alignment: .leading, spacing: 24) { + Group { + Section { + Text("Effective Date: \(AppConstants.termsOfServiceEffectiveDate)") + .font(.caption) + .foregroundColor(.secondary) + + Text("Welcome to GitStreak! These Terms of Service (\"Terms\") govern your use of our iOS application. By using GitStreak, you agree to be bound by these Terms.") + .font(.body) + } + + Section { + Text("1. Acceptance of Terms") + .font(.headline) + + Text("By downloading, installing, or using GitStreak, you acknowledge that you have read, understood, and agree to be bound by these Terms. If you do not agree to these Terms, please do not use the app.") + .font(.body) + } + + Section { + Text("2. Description of Service") + .font(.headline) + + Text("GitStreak is a personal productivity tool that:\n• Tracks your GitHub contribution activity\n• Displays commit streaks and statistics\n• Provides insights into your coding habits\n• Requires a valid GitHub account and Personal Access Token") + .font(.body) + } + + Section { + Text("3. User Requirements") + .font(.headline) + + Text("To use GitStreak, you must:\n• Be at least 13 years of age\n• Have a valid GitHub account\n• Generate and provide a GitHub Personal Access Token\n• Comply with GitHub's Terms of Service") + .font(.body) + } + + Section { + Text("4. User Responsibilities") + .font(.headline) + + Text("You are responsible for:\n• Maintaining the confidentiality of your GitHub token\n• All activities that occur under your account\n• Ensuring your use complies with all applicable laws\n• Not attempting to reverse engineer or hack the app") + .font(.body) + } + } + + Group { + Section { + Text("5. Intellectual Property") + .font(.headline) + + Text("GitStreak and its original content, features, and functionality are owned by the app developers and are protected by international copyright, trademark, and other intellectual property laws.") + .font(.body) + } + + Section { + Text("6. Third-Party Services") + .font(.headline) + + Text("GitStreak integrates with GitHub's API. Your use of GitHub's services is subject to GitHub's own Terms of Service. We are not responsible for the availability or reliability of GitHub's services.") + .font(.body) + } + + Section { + Text("7. Disclaimer of Warranties") + .font(.headline) + + Text("THE APP IS PROVIDED \"AS IS\" AND \"AS AVAILABLE\" WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. WE DO NOT WARRANT THAT THE APP WILL BE UNINTERRUPTED, ERROR-FREE, OR FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS.") + .font(.body) + .font(.system(.body, design: .monospaced)) + } + + Section { + Text("8. Limitation of Liability") + .font(.headline) + + Text("TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL WE BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING OUT OF OR RELATING TO YOUR USE OF THE APP.") + .font(.body) + .font(.system(.body, design: .monospaced)) + } + + Section { + Text("9. Indemnification") + .font(.headline) + + Text("You agree to indemnify and hold harmless GitStreak and its developers from any claims, losses, damages, liabilities, and expenses arising from your use of the app or violation of these Terms.") + .font(.body) + } + } + + Group { + Section { + Text("10. Termination") + .font(.headline) + + Text("We reserve the right to terminate or suspend your access to the app at any time, without prior notice, for conduct that we believe violates these Terms or is harmful to other users or the app.") + .font(.body) + } + + Section { + Text("11. Changes to Terms") + .font(.headline) + + Text("We may modify these Terms at any time. We will notify users of any material changes by updating the \"Effective Date\" at the top. Your continued use of the app after changes constitutes acceptance of the modified Terms.") + .font(.body) + } + + Section { + Text("12. Governing Law") + .font(.headline) + + Text("These Terms shall be governed by and construed in accordance with the laws of the jurisdiction in which the app developers are located, without regard to conflict of law provisions.") + .font(.body) + } + + Section { + Text("13. Contact Information") + .font(.headline) + + Text("For questions about these Terms, please contact us through the Help & Support section in the app.") + .font(.body) + } + } + } + .padding(24) + } + .background(Color(.systemGroupedBackground)) + .navigationTitle("Terms of Service") + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Done") { + dismiss() + } + } + } + } + } +} + +#Preview { + TermsOfServiceView() +} \ No newline at end of file