From 9378db7b24716c54f29c6dbc693c7600cddecb32 Mon Sep 17 00:00:00 2001 From: dadachi Date: Wed, 1 Apr 2026 16:24:54 +0900 Subject: [PATCH] Clean up Settings: remove Discussions, update contact, remove dead code Replace MFMailComposeViewController with mailto URL for contact. Replace App Store review link with StoreKit requestReview(). Remove Discussions link, MailView.swift, GlassButtonStyle.swift, and unused constants. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 2 +- NativeAppTemplate.xcodeproj/project.pbxproj | 16 ----- NativeAppTemplate/Constants.swift | 3 - .../UI/Settings/SettingsView.swift | 55 +++++++------- .../UI/Settings/SettingsViewModel.swift | 5 -- .../UI/Shared/GlassButtonStyle.swift | 50 ------------- NativeAppTemplate/UI/UIKit/MailView.swift | 72 ------------------- .../UI/Settings/SettingsViewModelTest.swift | 22 ------ 8 files changed, 30 insertions(+), 195 deletions(-) delete mode 100644 NativeAppTemplate/UI/Shared/GlassButtonStyle.swift delete mode 100644 NativeAppTemplate/UI/UIKit/MailView.swift diff --git a/CLAUDE.md b/CLAUDE.md index d900a23..fc3cc0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,7 @@ swiftformat . ### MVVM with Observable Pattern The app uses iOS 17's `@Observable` macro for state management with clean separation between: -- **Views**: SwiftUI views (99% SwiftUI, UIKit only for mail view) +- **Views**: SwiftUI views - **ViewModels**: Observable state containers that bridge views and data - **Models**: Domain objects and data structures - **Repositories**: Data access layer implementing CRUD operations diff --git a/NativeAppTemplate.xcodeproj/project.pbxproj b/NativeAppTemplate.xcodeproj/project.pbxproj index e77cf83..935f07a 100644 --- a/NativeAppTemplate.xcodeproj/project.pbxproj +++ b/NativeAppTemplate.xcodeproj/project.pbxproj @@ -156,7 +156,6 @@ 01E0A5B725BD0FCD00298D35 /* OfflineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A5B225BD0FC700298D35 /* OfflineView.swift */; }; 01E0A5B825BD0FCD00298D35 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A5B325BD0FC700298D35 /* ErrorView.swift */; }; 01E0A60125BD149200298D35 /* MainButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A5F925BD148800298D35 /* MainButtonView.swift */; }; - A1B2C3D401000001 /* GlassButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D401000002 /* GlassButtonStyle.swift */; }; A1B2C3D401000003 /* GlassCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D401000004 /* GlassCard.swift */; }; 01E0A60C25BD440300298D35 /* SignInEmailAndPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A60B25BD440300298D35 /* SignInEmailAndPasswordView.swift */; }; 01E0A63025BD53FD00298D35 /* Shop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A62F25BD53FD00298D35 /* Shop.swift */; }; @@ -166,7 +165,6 @@ 01E727212B020ECC004AC043 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E727202B020ECC004AC043 /* Bundle+Extensions.swift */; }; 01ED197B2A037B9E00CD4735 /* AppTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ED197A2A037B9E00CD4735 /* AppTabView.swift */; }; 01EE363E29A6DCEB009BCD9D /* ShopkeeperEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EE363D29A6DCEB009BCD9D /* ShopkeeperEditView.swift */; }; - 01FA23A12B00CE5700F1D446 /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01FA23A02B00CE5700F1D446 /* MailView.swift */; }; 01FC03E22B3329B700E6CD8E /* NeedAppUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01FC03E12B3329B700E6CD8E /* NeedAppUpdatesView.swift */; }; 7249A60C06FE44338E16BC50 /* CertificatePinningDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C597C0551370446BB931F19B /* CertificatePinningDelegate.swift */; }; /* End PBXBuildFile section */ @@ -332,7 +330,6 @@ 01E0A5B325BD0FC700298D35 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 01E0A5B525BD0FC700298D35 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 01E0A5F925BD148800298D35 /* MainButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainButtonView.swift; sourceTree = ""; }; - A1B2C3D401000002 /* GlassButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassButtonStyle.swift; sourceTree = ""; }; A1B2C3D401000004 /* GlassCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassCard.swift; sourceTree = ""; }; 01E0A60B25BD440300298D35 /* SignInEmailAndPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInEmailAndPasswordView.swift; sourceTree = ""; }; 01E0A62F25BD53FD00298D35 /* Shop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shop.swift; sourceTree = ""; }; @@ -342,7 +339,6 @@ 01E727202B020ECC004AC043 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; 01ED197A2A037B9E00CD4735 /* AppTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTabView.swift; sourceTree = ""; }; 01EE363D29A6DCEB009BCD9D /* ShopkeeperEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopkeeperEditView.swift; sourceTree = ""; }; - 01FA23A02B00CE5700F1D446 /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = ""; }; 01FC03E12B3329B700E6CD8E /* NeedAppUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeedAppUpdatesView.swift; sourceTree = ""; }; C597C0551370446BB931F19B /* CertificatePinningDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificatePinningDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -492,14 +488,6 @@ path = "Shop List"; sourceTree = ""; }; - 016EF40325E1E6630038195C /* UIKit */ = { - isa = PBXGroup; - children = ( - 01FA23A02B00CE5700F1D446 /* MailView.swift */, - ); - path = UIKit; - sourceTree = ""; - }; 0172030625A9642D008FD63B /* Networking */ = { isa = PBXGroup; children = ( @@ -674,7 +662,6 @@ 01011B542864434900B70D04 /* Shop Detail */, 0158B9F525C16366008EC9D5 /* Shop List */, 01467355299901E50005423D /* Shop Settings */, - 016EF40325E1E6630038195C /* UIKit */, ); path = UI; sourceTree = ""; @@ -832,7 +819,6 @@ isa = PBXGroup; children = ( 0172788A2D7D936E00CE424F /* Tags */, - A1B2C3D401000002 /* GlassButtonStyle.swift */, A1B2C3D401000004 /* GlassCard.swift */, 01E0A5F925BD148800298D35 /* MainButtonView.swift */, ); @@ -1017,7 +1003,6 @@ 017278832D7D935700CE424F /* ImageSaver.swift in Sources */, 01D8AE8B2AB453C1009AFFBA /* ShopBasicSettingsView.swift in Sources */, 01E0A60125BD149200298D35 /* MainButtonView.swift in Sources */, - A1B2C3D401000001 /* GlassButtonStyle.swift in Sources */, A1B2C3D401000003 /* GlassCard.swift in Sources */, 0182D39A25B4424B001E881D /* LoggedInShopkeeperKeychainStore.swift in Sources */, 01ED197B2A037B9E00CD4735 /* AppTabView.swift in Sources */, @@ -1070,7 +1055,6 @@ 0172787B2D7D903500CE424F /* ItemTagAdapter.swift in Sources */, 010F86AE2621A2A900B6C62A /* ShopDetailView.swift in Sources */, 011F6DF1259EF16400BED22E /* App.swift in Sources */, - 01FA23A12B00CE5700F1D446 /* MailView.swift in Sources */, 017278092D7D4F7400CE424F /* Onboarding.swift in Sources */, 01467357299902230005423D /* ShopSettingsView.swift in Sources */, 017278792D7D900100CE424F /* ItemTagsRequest.swift in Sources */, diff --git a/NativeAppTemplate/Constants.swift b/NativeAppTemplate/Constants.swift index 9bf19a0..b898d3e 100644 --- a/NativeAppTemplate/Constants.swift +++ b/NativeAppTemplate/Constants.swift @@ -215,7 +215,6 @@ extension String { static let supportWebsiteUrl: String = "https://nativeapptemplate.com" static let howToUseUrl: String = "https://myturntag.com/how" static let faqsUrl: String = "https://nativeapptemplate.com/faqs" - static let discussionsUrl: String = "https://github.com/nativeapptemplate/NativeAppTemplate-Free-iOS/discussions" static let privacyPolicyUrl: String = "https://nativeapptemplate.com/privacy" static let termsOfUseUrl: String = "https://nativeapptemplate.com/terms" @@ -225,9 +224,7 @@ extension String { static let supportWebsite = "Support Website" static let howToUse = "How To Use" static let faqs = "FAQs" - static let discussions = "Discussions" static let rateApp = "Rate or Review the App" - static let emailUs = "Email Us" static let contact = "Contact" static let privacyPolicy = "Privacy Policy" static let termsOfUse = "Terms of Use" diff --git a/NativeAppTemplate/UI/Settings/SettingsView.swift b/NativeAppTemplate/UI/Settings/SettingsView.swift index 361bf80..4fc6c13 100644 --- a/NativeAppTemplate/UI/Settings/SettingsView.swift +++ b/NativeAppTemplate/UI/Settings/SettingsView.swift @@ -3,13 +3,14 @@ // NativeAppTemplate // -import MessageUI +import StoreKit import SwiftUI struct SettingsView: View { @Environment(DataManager.self) private var dataManager @Environment(MessageBus.self) private var messageBus @Environment(TabViewModel.self) private var tabViewModel + @Environment(\.requestReview) private var requestReview @State private var viewModel: SettingsViewModel init( @@ -64,18 +65,13 @@ struct SettingsView: View { Label(String.faqs, systemImage: "questionmark") } - Link(destination: URL(string: String.discussionsUrl)!) { - Label(String.discussions, systemImage: "bubble.left.and.bubble.right") + Link(destination: supportEmailURL) { + Label(String.contact, systemImage: "envelope") } Button { - MFMailComposeViewController.canSendMail() ? viewModel.isShowingMailView.toggle() : viewModel - .alertNoMail.toggle() + requestReview() } label: { - Label(String.contact, systemImage: "envelope") - } - - Link(destination: URL(string: "\(String.appStoreUrl)?action=write-review")!) { Label(String.rateApp, systemImage: "hand.thumbsup") } @@ -111,22 +107,29 @@ struct SettingsView: View { } .navigationTitle(String.settings) .navigationBarTitleDisplayMode(.inline) - .sheet(isPresented: $viewModel.isShowingMailView) { - let systemVersion = UIDevice.current.systemVersion - let device = Utility.deviceModel - - MailView( - result: $viewModel.result, - recipients: [String.supportMail], - subject: "\(Bundle.main.displayName) for iPhone support", - messageBody: "\n\n\n-----\n\(Bundle.main.displayName) " + - "\(Bundle.main.appVersionLong)\n\(device) " + - "(\(systemVersion))\n\(Locale.preferredLanguages[0])" - ) - } - .alert( - "NO MAIL SETUP", - isPresented: $viewModel.alertNoMail - ) {} + } + + var supportEmailURL: URL { + let appName = Bundle.main.displayName + let appVersion = "\(Bundle.main.appVersionLong)" + let device = Utility.deviceModel + let systemVersion = UIDevice.current.systemVersion + let locale = Locale.current + let region = locale.region?.identifier ?? "Unknown" + let language = locale.language.languageCode?.identifier ?? "Unknown" + + let body = """ + + + --- + App: \(appName) \(appVersion) + Device: \(device) + iOS: \(systemVersion) + Region: \(region) + Locale: \(language)-\(region) + """ + + let encodedBody = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + return URL(string: "mailto:\(String.supportMail)?body=\(encodedBody)")! } } diff --git a/NativeAppTemplate/UI/Settings/SettingsViewModel.swift b/NativeAppTemplate/UI/Settings/SettingsViewModel.swift index f027829..31424aa 100644 --- a/NativeAppTemplate/UI/Settings/SettingsViewModel.swift +++ b/NativeAppTemplate/UI/Settings/SettingsViewModel.swift @@ -3,16 +3,11 @@ // NativeAppTemplate // -import MessageUI import Observation -import SwiftUI @Observable @MainActor final class SettingsViewModel { - var isShowingMailView = false - var alertNoMail = false - var result: Result? private(set) var messageBus: MessageBus private let sessionController: SessionControllerProtocol diff --git a/NativeAppTemplate/UI/Shared/GlassButtonStyle.swift b/NativeAppTemplate/UI/Shared/GlassButtonStyle.swift deleted file mode 100644 index 88d5da6..0000000 --- a/NativeAppTemplate/UI/Shared/GlassButtonStyle.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// GlassButtonStyle.swift -// NativeAppTemplate -// - -import SwiftUI - -struct PrimaryGlassButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(.uiButtonLabelLarge) - .foregroundStyle(.glassForeground) - .padding(.vertical, NativeAppTemplateConstants.Spacing.sm) - .padding(.horizontal, NativeAppTemplateConstants.Spacing.md) - .frame(maxWidth: .infinity) - .background( - LinearGradient( - colors: [Color.accent, Color.accent.opacity(0.8)], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .clipShape(RoundedRectangle(cornerRadius: NativeAppTemplateConstants.CornerRadius.sm)) - .shadow( - color: Color.accent.opacity(NativeAppTemplateConstants.Glass.shadowOpacity), - radius: NativeAppTemplateConstants.Layout.shadowRadius - ) - .scaleEffect(configuration.isPressed ? 0.97 : 1.0) - .animation(.easeInOut(duration: NativeAppTemplateConstants.Animation.fast), value: configuration.isPressed) - } -} - -struct SecondaryGlassButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(.uiButtonLabelLarge) - .foregroundStyle(.accent) - .padding(.vertical, NativeAppTemplateConstants.Spacing.sm) - .padding(.horizontal, NativeAppTemplateConstants.Spacing.md) - .frame(maxWidth: .infinity) - .background(.ultraThinMaterial) - .clipShape(RoundedRectangle(cornerRadius: NativeAppTemplateConstants.CornerRadius.sm)) - .overlay( - RoundedRectangle(cornerRadius: NativeAppTemplateConstants.CornerRadius.sm) - .stroke(Color.accent, lineWidth: NativeAppTemplateConstants.Layout.borderWidth) - ) - .scaleEffect(configuration.isPressed ? 0.97 : 1.0) - .animation(.easeInOut(duration: NativeAppTemplateConstants.Animation.fast), value: configuration.isPressed) - } -} diff --git a/NativeAppTemplate/UI/UIKit/MailView.swift b/NativeAppTemplate/UI/UIKit/MailView.swift deleted file mode 100644 index 200ed74..0000000 --- a/NativeAppTemplate/UI/UIKit/MailView.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// MailView.swift -// NativeAppTemplate -// - -import AVFoundation -import Foundation -import MessageUI -import SwiftUI -import UIKit - -struct MailView: UIViewControllerRepresentable { - @Environment(\.presentationMode) var presentation - @Binding var result: Result? - var recipients = [String]() - var subject = "" - var messageBody = "" - var isHTML = false - - class Coordinator: NSObject, MFMailComposeViewControllerDelegate { - @Binding var presentation: PresentationMode - @Binding var result: Result? - - init( - presentation: Binding, - result: Binding?> - ) { - _presentation = presentation - _result = result - } - - func mailComposeController( - _: MFMailComposeViewController, - didFinishWith result: MFMailComposeResult, - error: Error? - ) { - defer { - $presentation.wrappedValue.dismiss() - } - guard error == nil else { - self.result = .failure(error!) - return - } - self.result = .success(result) - - if result == .sent { - AudioServicesPlayAlertSound(SystemSoundID(1_001)) - } - } - } - - func makeCoordinator() -> Coordinator { - Coordinator( - presentation: presentation, - result: $result - ) - } - - func makeUIViewController(context: UIViewControllerRepresentableContext) -> MFMailComposeViewController { - let mfMailComposeViewController = MFMailComposeViewController() - mfMailComposeViewController.setToRecipients(recipients) - mfMailComposeViewController.setSubject(subject) - mfMailComposeViewController.setMessageBody(messageBody, isHTML: isHTML) - mfMailComposeViewController.mailComposeDelegate = context.coordinator - return mfMailComposeViewController - } - - func updateUIViewController( - _: MFMailComposeViewController, - context _: UIViewControllerRepresentableContext - ) {} -} diff --git a/NativeAppTemplateTests/UI/Settings/SettingsViewModelTest.swift b/NativeAppTemplateTests/UI/Settings/SettingsViewModelTest.swift index 8754806..474ea01 100644 --- a/NativeAppTemplateTests/UI/Settings/SettingsViewModelTest.swift +++ b/NativeAppTemplateTests/UI/Settings/SettingsViewModelTest.swift @@ -22,9 +22,6 @@ struct SettingsViewModelTest { messageBus: messageBus ) - #expect(viewModel.isShowingMailView == false) - #expect(viewModel.alertNoMail == false) - #expect(viewModel.result == nil) #expect(viewModel.messageBus === messageBus) } @@ -130,25 +127,6 @@ struct SettingsViewModelTest { #expect(tabViewModel.selectedTab == .shops) } - @Test - func statePropertiesAreObservable() { - let viewModel = SettingsViewModel( - sessionController: sessionController, - tabViewModel: tabViewModel, - messageBus: messageBus - ) - - // Test that properties can be set (indicating they're observable) - viewModel.isShowingMailView = true - #expect(viewModel.isShowingMailView == true) - - viewModel.alertNoMail = true - #expect(viewModel.alertNoMail == true) - - viewModel.result = .success(.sent) - #expect(viewModel.result != nil) - } - @Test func messageBusIsAccessible() { let viewModel = SettingsViewModel(