Merge branch 'feat/watch-outdoor' of https://git.nexus.hypnotruck.ch/sylvain/coach-ios into feat/watch-outdoor
This commit is contained in:
@@ -15,7 +15,8 @@ Instructions pour Claude Code sur ce projet. Lis ce fichier en début de session
|
||||
## 🔒 Règles non négociables
|
||||
|
||||
1. **Le code web n'est PAS ici** — pour modifier l'UI/UX, c'est `coach_sportif/web/`. Ce repo ne touche QUE le natif (Swift, Kotlin, plugins Capacitor, config).
|
||||
2. **Cookie inject AppDelegate** (iOS) : mécanisme critique pour l'auth WKWebView. Toute modif `AppDelegate.swift` doit préserver ce flux.
|
||||
2. **Cookie inject AppDelegate** (iOS) : mécanisme critique pour l'auth WKWebView. Toute modif `AppDelegate.swift` doit préserver ce flux. Il vit dans `didFinishLaunchingWithOptions`, qui **reste appelé** en cycle de vie par scènes et **avant** la connexion de la scène — Apple documente la séquence, l'ordre cookie → WebView est donc garanti.
|
||||
2bis. **⚠️ Cycle de vie UIScene depuis le 22/09/2026** (obligatoire iOS 27, Capacitor ≥ 8.5.2). Trois callbacks ont quitté `AppDelegate.swift` pour `SceneDelegate.swift`, et UIKit **ne les appelle plus sur l'AppDelegate même si on les y remet** : retour OAuth Google (`scene(_:openURLContexts:)`), Universal Links (`scene(_:continue:)`), flush des saisies widget (`sceneDidBecomeActive`). Aucun ne produit d'erreur de compilation s'il est mal placé — la panne est silencieuse. Le `SceneDelegate` instancie **`MainViewController`**, jamais un `CAPBridgeViewController` nu comme le template Capacitor : la sous-classe porte l'enregistrement des plugins et la config WKWebView. ⛔ Ne pas lancer `npx cap migrate` sur ce projet (le guide officiel exclut les AppDelegate à gestion d'URL personnalisée), ni `npm audit fix --force` (il redescend le CLI en v7 et supprime le support UIScene).
|
||||
3. **Health Connect Android** : nécessite la privacy policy URL dans le manifest (validation Play Console).
|
||||
4. **Pas d'extrapolation** : root cause vérifiable avant tout fix natif. iOS+Android = doubles surfaces de bug.
|
||||
5. **Default yes** : décider et exécuter (sauf actions destructives).
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
E547F675B2F61C3475D2A787 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93DA6B121E94B11DF938C90 /* LiveActivityManager.swift */; };
|
||||
F1ABCDEF0123456789AB0001 /* CoachHealthRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */; };
|
||||
F1ABCDEF0123456789AB0003 /* CoachSleep.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1ABCDEF0123456789AB0004 /* CoachSleep.swift */; };
|
||||
60BB59C4096F4C8EB22DE183 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82F240520FD4C6CB4A80522 /* SceneDelegate.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -195,6 +196,7 @@
|
||||
F1ABCDEF0123456789AB0004 /* CoachSleep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachSleep.swift; sourceTree = "<group>"; };
|
||||
F6BEAEF6F6274D61CAF22636 /* CoachWatchApp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachWatchApp.swift; sourceTree = "<group>"; };
|
||||
FBFCA969A1B083CD3178D9B2 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
B82F240520FD4C6CB4A80522 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
@@ -208,7 +210,18 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
880A76D4302B601000F582D5 /* CoachWatchWidgets */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (880A76E2302B601100F582D5 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = CoachWatchWidgets; sourceTree = "<group>"; };
|
||||
880A76D4302B601000F582D5 /* CoachWatchWidgets */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
880A76E2302B601100F582D5 /* PBXFileSystemSynchronizedBuildFileExceptionSet */,
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = CoachWatchWidgets;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -330,6 +343,7 @@
|
||||
6030BD533EBC2D1D26DB9479 /* LiveStore.swift */,
|
||||
340B99D5EA4DB985D7DFDA10 /* CoachLiveView.swift */,
|
||||
B93DA6B121E94B11DF938C90 /* LiveActivityManager.swift */,
|
||||
B82F240520FD4C6CB4A80522 /* SceneDelegate.swift */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
@@ -589,6 +603,7 @@
|
||||
548F8BFA06417B35474BD69E /* CoachLiveView.swift in Sources */,
|
||||
D1384CFA4960060CDBF05FC3 /* CoachLiveActivityAttributes.swift in Sources */,
|
||||
E547F675B2F61C3475D2A787 /* LiveActivityManager.swift in Sources */,
|
||||
60BB59C4096F4C8EB22DE183 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,25 @@
|
||||
<string>location</string>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
||||
81
ios/App/App/SceneDelegate.swift
Normal file
81
ios/App/App/SceneDelegate.swift
Normal file
@@ -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<UIOpenURLContext>) {
|
||||
// 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() }
|
||||
}
|
||||
}
|
||||
40
package-lock.json
generated
40
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user