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"
}
}