You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
PadelClub/PadelClub/PadelClubApp.swift

277 lines
9.8 KiB

//
// PadelClubApp.swift
// PadelClub
//
// Created by Laurent Morvillier on 02/02/2024.
//
import SwiftUI
import LeStorage
import TipKit
import PadelClubData
@main
struct PadelClubApp: App {
let persistenceController = PersistenceController.shared
@State private var navigationViewModel = NavigationViewModel()
@StateObject var networkMonitor: NetworkMonitor = NetworkMonitor()
@StateObject var dataStore = DataStore.shared
@State private var registrationError: RegistrationError? = nil
@State private var importObserverViewModel = ImportObserver()
@State private var showDisconnectionAlert: Bool = false
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State var requiredVersion: String? = nil
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var presentError: Binding<Bool> {
Binding {
registrationError != nil
} set: { value in
if value == false {
registrationError = nil
}
}
}
enum RegistrationError: LocalizedError {
case badActivationLink
case activationFailed
var errorDescription: String? {
switch self {
case .badActivationLink:
return "Le lien d'activation n'a pas fonctionné"
case .activationFailed:
return "L'activation de votre compte n'a pas fonctionné"
}
}
}
static var appVersion: String {
let dictionary = Bundle.main.infoDictionary!
let version = dictionary["CFBundleShortVersionString"] as! String
let build = dictionary["CFBundleVersion"] as! String
#if DEBUG
return "\(version) (\(build)) Debug"
#elseif TESTFLIGHT
return "\(version) (\(build)) TestFlight"
#elseif PRODTEST
return "\(version) (\(build)) ProdTest"
#else
return "\(version) (\(build))"
#endif
}
var body: some Scene {
WindowGroup {
if let requiredVersion {
DownloadNewVersionView(version: requiredVersion)
} else {
MainView()
.environment(\.horizontalSizeClass, .compact)
.alert(isPresented: presentError, error: registrationError) {
Button("Contactez-nous") {
_openMail()
}
Button("Annuler", role: .cancel) {
registrationError = nil
}
}
.onOpenURL { url in
#if targetEnvironment(simulator)
#else
_handleIncomingURL(url)
#endif
}
.environmentObject(networkMonitor)
.environmentObject(dataStore)
.environment(importObserverViewModel)
.environment(navigationViewModel)
.accentColor(.master)
.onAppear {
self._checkVersion()
if ManualPatcher.patchIfPossible(.disconnect) == true {
self.showDisconnectionAlert = true
}
#if DEBUG
print("Running in Debug mode")
#elseif TESTFLIGHT
print("Running in TestFlight mode")
#elseif PRODTEST
print("Running in ProdTest mode")
#else
print("Running in Release mode")
#endif
print(URLs.main.url)
networkMonitor.checkConnection()
self._onAppear()
print(PersistenceController.getModelVersion())
}
.alert(isPresented: self.$showDisconnectionAlert, content: {
Alert(title: Text("Vous avez été déconnecté. Veuillez vous reconnecter pour récupérer vos données."))
})
.task {
// try? Tips.resetDatastore()
try? Tips.configure([
.displayFrequency(.immediate),
.datastoreLocation(.applicationDefault)
])
}
.environment(\.managedObjectContext, persistenceController.localContainer.viewContext)
}
}
}
fileprivate func _checkVersion() {
Task.detached(priority: .high) {
if let requiredVersion = await self._retrieveRequiredVersion() {
let cleanedRequired = requiredVersion.replacingOccurrences(of: "\n", with: "")
Logger.log(">>> REQUIRED VERSION = \(requiredVersion)")
if let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
await MainActor.run {
if VersionComparator.compare(cleanedRequired, currentVersion) == 1 {
self.requiredVersion = cleanedRequired
}
}
}
}
}
}
fileprivate func _retrieveRequiredVersion() async -> String? {
let requiredVersionURL = URLs.main.extend(path: "static/misc/required-version.txt")
do {
let (data, _) = try await URLSession.shared.data(from: requiredVersionURL)
return String(data: data, encoding: .utf8)
} catch {
Logger.log("Error fetching required version: \(error)")
return nil
}
}
private func _handleIncomingURL(_ url: URL) {
// Parse the URL
let pathComponents = url.pathComponents
if url.scheme == "padelclub" && pathComponents.count > 3 && pathComponents[1] == "activate" {
let uidb64 = pathComponents[2]
let token = pathComponents[3]
_activateUser(uidb64: uidb64, token: token)
print(uidb64, token)
} else {
// Handle invalid URL case
print("Handle invalid URL case")
registrationError = .badActivationLink
}
}
private func _activateUser(uidb64: String, token: String) {
guard let url = URL(string: "https://\(URLs.activationHost.rawValue)/activate/\(uidb64)/\(token)/") else {
registrationError = .activationFailed
return
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data, let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
print("activation error")
print(error)
registrationError = .activationFailed
return
}
DispatchQueue.main.async {
dataStore.appSettings.didRegisterAccount = true
dataStore.appSettingsStorage.write()
if navigationViewModel.selectedTab != .umpire {
navigationViewModel.selectedTab = .umpire
}
if navigationViewModel.accountPath.isEmpty {
navigationViewModel.accountPath.append(MyAccountView.AccountScreen.login)
} else if navigationViewModel.accountPath.last! != .login {
navigationViewModel.accountPath.removeAll()
navigationViewModel.accountPath.append(MyAccountView.AccountScreen.login)
}
}
}.resume()
}
fileprivate func _onAppear() {
let docURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
Logger.log("doc dir = \(docURL.absoluteString)")
UserDefaults.standard.set(false, forKey: "_UIConstraintBasedLayoutLogUnsatisfiable")
// AppDelegate.askPermissions()
}
private func _openMail(emailTo: String = "support@padelclub.app", subject: String = "Support Padel Club") {
if let url = URL(string: "mailto:\(emailTo)?subject=\(subject)"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
}
struct DownloadNewVersionView: View {
var version: String
var body: some View {
VStack {
// AngledStripesBackground()
Spacer()
VStack(spacing: 20.0) {
Text("Veuillez télécharger la nouvelle version de Padel Club pour continuer à vous servir de l'app !")
.fontWeight(.semibold)
.foregroundStyle(.white)
.padding()
.background(.logoRed)
.clipShape(.buttonBorder)
// .padding(32.0)
VStack(alignment: .center, spacing: 0.0
) {
Text("Version \(self.version)")
.fontWeight(.bold)
Image(systemName: "square.and.arrow.down").font(.title)
}.padding().background(.logoYellow)
.clipShape(.buttonBorder)
}
.frame(maxWidth: .infinity)
.foregroundStyle(.logoBackground)
.fontWeight(.medium)
.multilineTextAlignment(.center)
.padding(.horizontal, 36.0)
Image("logo").padding(.vertical, 50.0)
Spacer()
}
.background(.logoBackground)
.onTapGesture {
UIApplication.shared.open(URLs.appStore.url)
}
}
}
struct DisconnectionAlertView: View {
var body: some View {
Text("Vous avez été déconnecté. Veuillez vous reconnecter pour récupérer vos données.").multilineTextAlignment(.center).padding()
}
}
#Preview {
DownloadNewVersionView(version: "1.2")
}