diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 44712632..8b19e5f9 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -63,6 +63,8 @@ 1F943C702927F90400ABE095 /* BrowseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5BA2927E0F800B8D837 /* BrowseView.swift */; }; 1F943C712927F90400ABE095 /* MyAppsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5BD2927E10D00B8D837 /* MyAppsView.swift */; }; 1FA1C8CA294906890083119D /* MyAppsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA1C8C9294906890083119D /* MyAppsViewModel.swift */; }; + 1FA5A6CA298E8B2F007BA946 /* RefreshAttemptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA5A6C9298E8B2F007BA946 /* RefreshAttemptsView.swift */; }; + 1FA5A6CC298E8FE4007BA946 /* MailComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA5A6CB298E8FE4007BA946 /* MailComposeView.swift */; }; 1FB84BA62928DE08006A5CF4 /* AppDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB84BA52928DE08006A5CF4 /* AppDetailView.swift */; }; 1FB96FBE292A20E5007E68D1 /* ObservableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FBD292A20E5007E68D1 /* ObservableScrollView.swift */; }; 1FB96FC0292A63F2007E68D1 /* AppPillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FBF292A63F2007E68D1 /* AppPillButton.swift */; }; @@ -604,6 +606,8 @@ 1F943C652927F36600ABE095 /* NewsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsViewModel.swift; sourceTree = ""; }; 1F943C672927F39400ABE095 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; 1FA1C8C9294906890083119D /* MyAppsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewModel.swift; sourceTree = ""; }; + 1FA5A6C9298E8B2F007BA946 /* RefreshAttemptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAttemptsView.swift; sourceTree = ""; }; + 1FA5A6CB298E8FE4007BA946 /* MailComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailComposeView.swift; sourceTree = ""; }; 1FAFC5A42927E00000B8D837 /* SideStoreUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideStoreUIApp.swift; sourceTree = ""; }; 1FAFC5B52927E06300B8D837 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; 1FAFC5B82927E0EE00B8D837 /* NewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsView.swift; sourceTree = ""; }; @@ -1115,6 +1119,7 @@ 1F66F5B92938CA5700A910CA /* VisualEffectView.swift */, 1F6284D6295218980060AAD8 /* DocumentPicker.swift */, 1F545E84298D84CF00589F68 /* FilePreviewView.swift */, + 1FA5A6CB298E8FE4007BA946 /* MailComposeView.swift */, ); path = "UIView Representables"; sourceTree = ""; @@ -1200,6 +1205,7 @@ 1F0DD83E29367F6C007608A4 /* ConnectAppleIDView.swift */, 1F2EF786297C4D40002FD839 /* LicensesView.swift */, 1F545E82298D79E400589F68 /* ErrorLogView.swift */, + 1FA5A6C9298E8B2F007BA946 /* RefreshAttemptsView.swift */, ); path = Settings; sourceTree = ""; @@ -2876,6 +2882,7 @@ BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */, BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */, 1F66F5BC2938F03700A910CA /* Modifiers.swift in Sources */, + 1FA5A6CA298E8B2F007BA946 /* RefreshAttemptsView.swift in Sources */, 1F5DF9D82974426300DDAA47 /* AppScreenshot.swift in Sources */, 1F66F5BA2938CA5700A910CA /* VisualEffectView.swift in Sources */, BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */, @@ -2902,6 +2909,7 @@ 1FB84BA62928DE08006A5CF4 /* AppDetailView.swift in Sources */, D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */, BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */, + 1FA5A6CC298E8FE4007BA946 /* MailComposeView.swift in Sources */, BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */, BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */, 1FB96FC5292A7251007E68D1 /* BrowseAppPreviewView.swift in Sources */, diff --git a/AltStore/Manager/NotificationManager.swift b/AltStore/Manager/NotificationManager.swift index af6d1e8a..032d03e1 100644 --- a/AltStore/Manager/NotificationManager.swift +++ b/AltStore/Manager/NotificationManager.swift @@ -65,7 +65,7 @@ class NotificationManager: ObservableObject { self.showNotification(title: text, detailText: detailText) } - func showNotification(title: String, detailText: String?) { + func showNotification(title: String, detailText: String? = nil) { let notificationId = UUID() DispatchQueue.main.async { diff --git a/AltStore/View Extensions/UIView Representables/MailComposeView.swift b/AltStore/View Extensions/UIView Representables/MailComposeView.swift new file mode 100644 index 00000000..a773befc --- /dev/null +++ b/AltStore/View Extensions/UIView Representables/MailComposeView.swift @@ -0,0 +1,71 @@ +// +// MailComposeView.swift +// SideStore +// +// Created by Fabian Thies on 04.02.23. +// Copyright © 2023 SideStore. All rights reserved. +// + +import SwiftUI +import MessageUI + +struct MailComposeView: UIViewControllerRepresentable { + typealias ActionHandler = () -> Void + typealias ErrorHandler = (Error) -> Void + + static var canSendMail: Bool { + MFMailComposeViewController.canSendMail() + } + + let recipients: [String] + let subject: String + var body: String? = nil + + var onMailSent: ActionHandler? = nil + var onError: ErrorHandler? = nil + + func makeCoordinator() -> Coordinator { + Coordinator(mailSentHandler: self.onMailSent, errorHandler: self.onError) + } + + func makeUIViewController(context: Context) -> some UIViewController { + let mailViewController = MFMailComposeViewController() + mailViewController.mailComposeDelegate = context.coordinator + mailViewController.setToRecipients(self.recipients) + mailViewController.setSubject(self.subject) + + if let body { + mailViewController.setMessageBody(body, isHTML: false) + } + + return mailViewController + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + + } +} + +extension MailComposeView { + class Coordinator: NSObject, MFMailComposeViewControllerDelegate { + + let mailSentHandler: ActionHandler? + let errorHandler: ErrorHandler? + + init(mailSentHandler: ActionHandler?, errorHandler: ErrorHandler?) { + self.mailSentHandler = mailSentHandler + self.errorHandler = errorHandler + super.init() + } + + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + if result == .sent, let mailSentHandler { + mailSentHandler() + } else if result == .failed, let errorHandler, let error { + errorHandler(error) + } + + controller.dismiss(animated: true) + } + } +} diff --git a/AltStore/Views/Settings/RefreshAttemptsView.swift b/AltStore/Views/Settings/RefreshAttemptsView.swift new file mode 100644 index 00000000..24d8abfd --- /dev/null +++ b/AltStore/Views/Settings/RefreshAttemptsView.swift @@ -0,0 +1,90 @@ +// +// RefreshAttemptsView.swift +// SideStore +// +// Created by Fabian Thies on 04.02.23. +// Copyright © 2023 SideStore. All rights reserved. +// + +import SwiftUI +import AltStoreCore + +struct RefreshAttemptsView: View { + @SwiftUI.FetchRequest(sortDescriptors: [ + NSSortDescriptor(keyPath: \RefreshAttempt.date, ascending: false) + ]) + var refreshAttempts: FetchedResults + + var groupedRefreshAttempts: [Date: [RefreshAttempt]] { + Dictionary(grouping: refreshAttempts, by: { Calendar.current.startOfDay(for: $0.date) }) + } + + var body: some View { + List { + ForEach(groupedRefreshAttempts.keys.sorted(by: { $0 > $1 }), id: \.self) { date in + Section { + let attempts = groupedRefreshAttempts[date] ?? [] + ForEach(attempts, id: \.date) { attempt in + VStack(alignment: .leading, spacing: 8) { + HStack { + if attempt.isSuccess { + Text("Success") + .bold() + .foregroundColor(.green) + } else { + Text("Failure") + .bold() + .foregroundColor(.red) + } + + Spacer() + + Text(DateFormatterHelper.timeString(for: attempt.date)) + .font(.caption) + .foregroundColor(.secondary) + } + + if let description = attempt.errorDescription { + Text(description) + } + } + } + } header: { + Text(DateFormatterHelper.string(for: date)) + } + } + } + .background(self.listBackground) + .navigationTitle("Refresh Attempts") + } + + @ViewBuilder + var listBackground: some View { + if self.refreshAttempts.isEmpty { + VStack(spacing: 8) { + Spacer() + Text("No Refresh Attempts") + .font(.title) + + Text("The more you use SideStore, the more often iOS will allow it to refresh apps in the background.") + + Spacer() + } + .multilineTextAlignment(.center) + .foregroundColor(.secondary) + .padding() + } else { + Color.clear + } + } +} + + +struct RefreshAttemptsView_Previews: PreviewProvider { + + static var previews: some View { + NavigationView { + RefreshAttemptsView() + } + } +} diff --git a/AltStore/Views/Settings/SettingsView.swift b/AltStore/Views/Settings/SettingsView.swift index 88143292..5d51df7c 100644 --- a/AltStore/Views/Settings/SettingsView.swift +++ b/AltStore/Views/Settings/SettingsView.swift @@ -27,6 +27,8 @@ struct SettingsView: View { @State var isShowingConnectAppleIDView = false @State var isShowingAddShortcutView = false + @State var isShowingFeedbackMailView = false + @State var isShowingResetPairingFileConfirmation = false @State var externalURLToShow: URL? @@ -147,18 +149,45 @@ struct SettingsView: View { } Section { - SwiftUI.Button(action: switchToUIKit) { - Text(L10n.SettingsView.switchToUIKit) - } - - SwiftUI.Button(action: resetImageCache) { - Text(L10n.SettingsView.resetImageCache) - } - - NavigationLink { + NavigationLink("Show Error Log") { ErrorLogView() - } label: { - Text("Show Error Log") + } + + NavigationLink("Show Refresh Attempts") { + RefreshAttemptsView() + } + + if MailComposeView.canSendMail { + SwiftUI.Button("Send Feedback") { + self.isShowingFeedbackMailView = true + } + .sheet(isPresented: self.$isShowingFeedbackMailView) { + MailComposeView(recipients: ["support@sidestore.io"], + subject: "SideStore Beta \(appVersion) Feedback") { + NotificationManager.shared.showNotification(title: "Thank you for your feedback!") + } onError: { error in + NotificationManager.shared.reportError(error: error) + } + .ignoresSafeArea() + } + } + + SwiftUI.Button(L10n.SettingsView.switchToUIKit, action: self.switchToUIKit) + + SwiftUI.Button("Advanced Settings", action: self.showAdvancedSettings) + + SwiftUI.Button(L10n.SettingsView.resetImageCache, action: self.resetImageCache) + .foregroundColor(.red) + + SwiftUI.Button("Reset Pairing File") { + self.isShowingResetPairingFileConfirmation = true + } + .foregroundColor(.red) + .actionSheet(isPresented: self.$isShowingResetPairingFileConfirmation) { + ActionSheet(title: Text("Are you sure to reset the pairing file?"), message: Text("You can reset the pairing file when you cannot sideload apps or enable JIT. SideStore will close when the file has been deleted."), buttons: [ + .destructive(Text("Delete and Reset"), action: self.resetPairingFile), + .cancel() + ]) } } header: { Text(L10n.SettingsView.debug) @@ -231,7 +260,6 @@ struct SettingsView: View { } } - func switchToUIKit() { let storyboard = UIStoryboard(name: "Main", bundle: .main) let rootVC = storyboard.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController @@ -251,6 +279,37 @@ struct SettingsView: View { fatalError("\(error)") } } + + func resetPairingFile() { + let filename = "ALTPairingFile.mobiledevicepairing" + let fileURL = FileManager.default.documentsDirectory.appendingPathComponent(filename) + + // Delete the pairing file if it exists + if FileManager.default.fileExists(atPath: fileURL.path) { + do { + try FileManager.default.removeItem(at: fileURL) + print("Pairing file deleted successfully.") + } catch { + print("Failed to delete pairing file:", error) + } + } + + // Close and exit SideStore + UIApplication.shared.perform(#selector(URLSessionTask.suspend)) + DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: .milliseconds(500))) { + exit(0) + } + } + + func showAdvancedSettings() { + // Create the URL that deep links to our app's custom settings. + guard let url = URL(string: UIApplication.openSettingsURLString) else { + return + } + + // Ask the system to open that URL. + UIApplication.shared.open(url) + } } struct SettingsView_Previews: PreviewProvider {