diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index 6575b45..54b8077 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -1,12 +1,11 @@ import UIKit import Capacitor -#if canImport(GoogleSignIn) -import GoogleSignIn -#endif @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { + // Conservée comme dans le template Capacitor 8.5. En cycle par scènes, + // c'est le SceneDelegate qui possède la fenêtre affichée. var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -47,28 +46,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate { HTTPCookieStorage.shared.setCookie(cookie) } - func applicationWillResignActive(_ application: UIApplication) {} - func applicationDidEnterBackground(_ application: UIApplication) {} - func applicationWillEnterForeground(_ application: UIApplication) {} - func applicationDidBecomeActive(_ application: UIApplication) { - // Les saisies faites depuis le widget (eau, café) attendent dans l'App - // Group : une extension n'a ni session ni droit de tenir une requête. - // On les transmet ici, à chaque retour au premier plan. - Task { await CoachQuickSync.flush() } + // ─── Cycle de vie par scènes (obligatoire iOS 27) ─────────────────── + // + // ⚠️ Depuis l'adoption du `UIApplicationSceneManifest`, UIKit n'appelle + // PLUS `applicationDidBecomeActive`, `application(_:open:options:)` ni + // `application(_:continue:restorationHandler:)` — « even if you keep them + // implemented » (Apple, *Transitioning to the UIKit scene-based life + // cycle*). Les trois ont déménagé dans `SceneDelegate.swift` : + // + // applicationDidBecomeActive -> sceneDidBecomeActive (CoachQuickSync) + // application(_:open:options:) -> scene(_:openURLContexts:) (OAuth Google) + // application(_:continue:…) -> scene(_:continue:) (Universal Links) + // + // Elles ne sont pas conservées ici en doublon : une méthode morte qui a + // l'air vivante est exactement ce qui fait chercher la panne au mauvais + // endroit. Les callbacks de notification distante, eux, restent sur + // l'AppDelegate — `UISceneDelegate` n'en a aucun équivalent. + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + let config = UISceneConfiguration(name: "Default Configuration", + sessionRole: connectingSceneSession.role) + config.delegateClass = SceneDelegate.self + return config } + func applicationWillTerminate(_ application: UIApplication) {} - - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - #if canImport(GoogleSignIn) - // Retour du flux OAuth Google (reversed client id URL scheme). - if GIDSignIn.sharedInstance.handle(url) { - return true - } - #endif - return ApplicationDelegateProxy.shared.application(app, open: url, options: options) - } - - func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) - } } diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index ab76977..452998b 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -50,6 +50,25 @@ location fetch + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/ios/App/App/SceneDelegate.swift b/ios/App/App/SceneDelegate.swift new file mode 100644 index 0000000..367209d --- /dev/null +++ b/ios/App/App/SceneDelegate.swift @@ -0,0 +1,81 @@ +import UIKit +import Capacitor +#if canImport(GoogleSignIn) +import GoogleSignIn +#endif + +/// Cycle de vie par scènes, **obligatoire** depuis iOS 27 : « apps built with +/// the latest SDK must adopt the scene-based life cycle or they fail to launch » +/// (Apple, *Transitioning to the UIKit scene-based life cycle*). Sans ce +/// fichier et le `UIApplicationSceneManifest` de l'Info.plist, l'app ne démarre +/// pas du tout — c'est un assert au lancement, pas un avertissement. +/// +/// ⚠️ **Ce qui arrive ici arrive en SILENCE.** Une fois le manifeste en place, +/// UIKit cesse d'appeler `application(_:open:options:)`, +/// `application(_:continue:restorationHandler:)` et `applicationDidBecomeActive` +/// sur l'AppDelegate — « even if you keep them implemented » (Apple). Rien ne +/// casse à la compilation : le retour OAuth Google part vers Safari et ne +/// revient jamais, et les saisies du widget restent bloquées dans l'App Group. +/// Les trois sont donc repris ci-dessous. +/// +/// Ce que la migration NE change PAS, et c'est ce qui la rendait acceptable : +/// `application(_:didFinishLaunchingWithOptions:)` reste appelé, et toujours +/// **avant** que la scène ne se connecte — donc avant que la WKWebView charge. +/// L'injection du cookie d'auth garde son ordre (Apple documente la séquence : +/// launch → création des scènes → `scene(_:willConnectTo:)`). Les callbacks de +/// notification distante ne bougent pas non plus : `UISceneDelegate` n'a aucun +/// équivalent, et ils ne sont pas dépréciés. +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = scene as? UIWindowScene else { return } + + // Le manifeste déclare `UISceneStoryboardFile = Main` : UIKit a donc + // déjà chargé Main.storyboard et instancié `MainViewController`, qui + // porte l'enregistrement des plugins natifs et la config WKWebView. + // + // Le template de Capacitor pose ici un `CAPBridgeViewController()` nu. + // L'appliquer tel quel remplacerait notre sous-classe et ferait perdre + // cette configuration **sans erreur visible**. On ne crée donc la + // fenêtre que si le storyboard ne l'a pas fait, et avec notre + // contrôleur. + if window == nil { + let fenetre = UIWindow(windowScene: windowScene) + fenetre.rootViewController = MainViewController() + window = fenetre + fenetre.makeKeyAndVisible() + } + + SceneDelegateProxy.shared.scene(scene, willConnectTo: session, options: connectionOptions) + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + // Retour du flux OAuth Google. `handle(url)` prend UNE url alors que la + // scène en livre un ensemble : on laisse à Capacitor ce que Google n'a + // pas consommé, plutôt que de tout lui passer ou de tout lui cacher. + var restants = URLContexts + #if canImport(GoogleSignIn) + restants = URLContexts.filter { !GIDSignIn.sharedInstance.handle($0.url) } + #endif + guard !restants.isEmpty else { return } + SceneDelegateProxy.shared.scene(scene, openURLContexts: restants) + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + // Universal Links : les .widgetURL des widgets, de la Live Activity et + // des complications Watch passent par là. Sans ce relais, ils + // ouvriraient Safari, où le cookie injecté dans la WKWebView n'existe + // pas. + SceneDelegateProxy.shared.scene(scene, continue: userActivity) + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Reprend `applicationDidBecomeActive`, qu'UIKit n'appelle plus. Les + // saisies faites depuis le widget (eau, café) attendent dans l'App + // Group : une extension n'a ni session ni droit de tenir une requête. + Task { await CoachQuickSync.flush() } + } +} diff --git a/package-lock.json b/package-lock.json index 752f665..5e06f5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,12 @@ "version": "0.1.0", "license": "UNLICENSED", "dependencies": { - "@capacitor/android": "^8.3.1", + "@capacitor/android": "^8.5.2", "@capacitor/app": "^8.1.0", - "@capacitor/core": "^8.3.1", + "@capacitor/core": "^8.5.2", "@capacitor/geolocation": "^8.2.0", "@capacitor/haptics": "^8.0.2", - "@capacitor/ios": "^8.3.1", + "@capacitor/ios": "^8.5.2", "@capacitor/local-notifications": "^8.0.2", "@capacitor/push-notifications": "^8.0.3", "@capacitor/splash-screen": "^8.0.1", @@ -24,7 +24,7 @@ }, "devDependencies": { "@capacitor/assets": "^3.0.5", - "@capacitor/cli": "^8.3.1" + "@capacitor/cli": "^8.5.2" } }, "node_modules/@babel/code-frame": { @@ -51,12 +51,12 @@ } }, "node_modules/@capacitor/android": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.3.1.tgz", - "integrity": "sha512-hjskIG8YcBEh3X4yaTXvE9gcqpdcxunTgFruSKnuPxtMxAUzEK4Oq25x0Z1g3cz+MQPc+lRG09R7Ovc+ydKsNw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.5.2.tgz", + "integrity": "sha512-rtiaz78egCcS+vs29wkWCL3C+RhazKMSygXcjutwWP8NmO6r5UBxOvGf3Wjz5+ahobyQ723tn9gCePwRwcNDZQ==", "license": "MIT", "peerDependencies": { - "@capacitor/core": "^8.3.0" + "@capacitor/core": "^8.5.0" } }, "node_modules/@capacitor/app": { @@ -452,10 +452,11 @@ "dev": true }, "node_modules/@capacitor/cli": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.3.1.tgz", - "integrity": "sha512-1sPGW4THTDfR6YjXwZ0jM7oAfAtciPOHN00qs/3sNAQx1kKrrEYSfDPwCm1/xlAgi0OeL69SiRfw314Ans+1sw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.5.2.tgz", + "integrity": "sha512-O2kpA5ZPJUAmliDCcmpMf8xnJwMezxlFPUrlyOAjbL3U9MFNY3l4ZpPI/UXwvyPmR8ETcVF9pFr+VdWq61rqTQ==", "dev": true, + "license": "MIT", "dependencies": { "@ionic/cli-framework-output": "^2.2.8", "@ionic/utils-subprocess": "^3.0.1", @@ -473,6 +474,7 @@ "semver": "^7.6.3", "tar": "^7.5.3", "tslib": "^2.8.1", + "xcode": "^3.0.1", "xml2js": "^0.6.2" }, "bin": { @@ -484,9 +486,10 @@ } }, "node_modules/@capacitor/core": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.1.tgz", - "integrity": "sha512-UF8ItlHguU1Z6GXfPTeT2gakf+ctNI8pAS1kwSBQlsJMlfD4OPoto/SmKnOxKCQvnF4WRcdWeg6C0zREUNaAQg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.5.2.tgz", + "integrity": "sha512-agnQbGdhGw/KZFuLf7H9SY2o+oE7upkr/WPQsYaokXP6f9chaE8iVmbKkfN6SVl4w3pmz7oZ3Ur1hbkmy1hx+w==", + "license": "MIT", "dependencies": { "tslib": "^2.1.0" } @@ -511,11 +514,12 @@ } }, "node_modules/@capacitor/ios": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.3.1.tgz", - "integrity": "sha512-BEhLyYYHWJLib4mpaPMaaylbC8meqgxbNYwQJH2svsSLW7yo/hFie+Zoo66a44XnqcMd2tvmAuzimWunXZi/xA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.5.2.tgz", + "integrity": "sha512-eOT2S67AfTFG7KN0T3oxwAajrJz4rSye+ODfvChiwrk0F/+kWufFM1qU6cDjGJUOfwdAoeuEpZyWHSwZlu4GFg==", + "license": "MIT", "peerDependencies": { - "@capacitor/core": "^8.3.0" + "@capacitor/core": "^8.5.0" } }, "node_modules/@capacitor/local-notifications": { diff --git a/package.json b/package.json index 7a5f6a8..13c968f 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,12 @@ "author": "Sylvain Bettinelli", "license": "UNLICENSED", "dependencies": { - "@capacitor/android": "^8.3.1", + "@capacitor/android": "^8.5.2", "@capacitor/app": "^8.1.0", - "@capacitor/core": "^8.3.1", + "@capacitor/core": "^8.5.2", "@capacitor/geolocation": "^8.2.0", "@capacitor/haptics": "^8.0.2", - "@capacitor/ios": "^8.3.1", + "@capacitor/ios": "^8.5.2", "@capacitor/local-notifications": "^8.0.2", "@capacitor/push-notifications": "^8.0.3", "@capacitor/splash-screen": "^8.0.1", @@ -30,6 +30,6 @@ }, "devDependencies": { "@capacitor/assets": "^3.0.5", - "@capacitor/cli": "^8.3.1" + "@capacitor/cli": "^8.5.2" } }