fix(ios): robustesse bridge widget + Apple Sign-In
- CoachWidgetBridge : lit le score en NSNumber.intValue (le cast `as? Int` renvoyait nil quand le web envoie un float → score widget perdu par intermittence). CoachWidgetStore.save renvoie un Bool ; le plugin fait call.reject si la sauvegarde échoue au lieu de résoudre ok:true à tort. - CoachAppleAuth : sur double-tap, rejette l'ancien pendingCall (Promise JS ne pend plus indéfiniment) ; accès pendingCall confiné au main thread ; erreur d'annulation testée par domaine ASAuthorizationError (pas rawValue nu). Swift pur, non compilé ici (pas de Xcode) — à valider au prochain build Mac. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ Les deux ne communiquent **que via git + ce fichier**. ⚠️ La mémoire de cha
|
|||||||
## 👉 À reprendre
|
## 👉 À reprendre
|
||||||
- ✅ **Phase 4 login — FAIT & DÉPLOYÉ** (révocation Apple à la suppression de compte) : code complet côté backend `coach_sportif` (commit `5ae6e2d`) + clé `.p8` déployée sur le VPS prod (vérifié 2026-06-26). Rien à coder. Détails : `coach_sportif/COWORK.md`.
|
- ✅ **Phase 4 login — FAIT & DÉPLOYÉ** (révocation Apple à la suppression de compte) : code complet côté backend `coach_sportif` (commit `5ae6e2d`) + clé `.p8` déployée sur le VPS prod (vérifié 2026-06-26). Rien à coder. Détails : `coach_sportif/COWORK.md`.
|
||||||
- **iOS — bloqué Mac** : builder + uploader TestFlight le natif accumulé (login Apple+Google natif, watchOS live, HeartRateRangeAlert). Sur le Mac : `cd ~/coach-ios && git pull && npm install && npx cap sync ios && open ios/App/App.xcworkspace` → Clean Build Folder → Archive → Upload.
|
- **iOS — bloqué Mac** : builder + uploader TestFlight le natif accumulé (login Apple+Google natif, watchOS live, HeartRateRangeAlert). Sur le Mac : `cd ~/coach-ios && git pull && npm install && npx cap sync ios && open ios/App/App.xcworkspace` → Clean Build Folder → Archive → Upload.
|
||||||
|
- ⚠️ **Fixes robustesse 2026-07-01 (dev, non compilés)** à valider au prochain build : `CoachWidgetBridge` (score lu en `NSNumber.intValue` → corrige un score float perdu ; `save` renvoie Bool → `call.reject` si App Group KO), `CoachAppleAuth` (double-tap : rejette l'ancien `pendingCall` ; erreur testée par domaine `ASAuthorizationError`). Swift pur, aucun nouveau fichier ni capability.
|
||||||
- **watchOS — bloqué Mac** : test device + TestFlight de la cible `CoachWatch` (Phases 1-3 réalisées, build vert sim). Cf. `HANDOFF-WATCHOS.md`.
|
- **watchOS — bloqué Mac** : test device + TestFlight de la cible `CoachWatch` (Phases 1-3 réalisées, build vert sim). Cf. `HANDOFF-WATCHOS.md`.
|
||||||
- **Android — bloqué externe** : Play Console ($25 validé), upload AAB + Internal Testing depuis machine avec Android SDK.
|
- **Android — bloqué externe** : Play Console ($25 validé), upload AAB + Internal Testing depuis machine avec Android SDK.
|
||||||
- Backend/web : voir `coach_sportif/COWORK.md`.
|
- Backend/web : voir `coach_sportif/COWORK.md`.
|
||||||
|
|||||||
@@ -38,8 +38,13 @@ public class CoachAppleAuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|||||||
call.reject("Sign in with Apple requires iOS 13+")
|
call.reject("Sign in with Apple requires iOS 13+")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.pendingCall = call
|
// Tout l'accès à pendingCall est confiné au main thread (les callbacks
|
||||||
|
// delegate arrivent aussi sur main) → pas de data race sur double-tap.
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
// Un signIn déjà en cours ? on rejette l'ancien pour ne pas laisser sa
|
||||||
|
// Promise JS pendante indéfiniment (double-tap, retry).
|
||||||
|
self.pendingCall?.reject("superseded")
|
||||||
|
self.pendingCall = call
|
||||||
let provider = ASAuthorizationAppleIDProvider()
|
let provider = ASAuthorizationAppleIDProvider()
|
||||||
let request = provider.createRequest()
|
let request = provider.createRequest()
|
||||||
request.requestedScopes = [.fullName, .email]
|
request.requestedScopes = [.fullName, .email]
|
||||||
@@ -83,8 +88,8 @@ extension CoachAppleAuthPlugin: ASAuthorizationControllerDelegate {
|
|||||||
controller: ASAuthorizationController,
|
controller: ASAuthorizationController,
|
||||||
didCompleteWithError error: Error
|
didCompleteWithError error: Error
|
||||||
) {
|
) {
|
||||||
let nsErr = error as NSError
|
// Vérifie le domaine ET le code (un code 1001 d'un autre domaine ≠ annulation).
|
||||||
if nsErr.code == ASAuthorizationError.canceled.rawValue {
|
if let authErr = error as? ASAuthorizationError, authErr.code == .canceled {
|
||||||
pendingCall?.reject("canceled")
|
pendingCall?.reject("canceled")
|
||||||
} else {
|
} else {
|
||||||
pendingCall?.reject("Apple sign-in failed: \(error.localizedDescription)")
|
pendingCall?.reject("Apple sign-in failed: \(error.localizedDescription)")
|
||||||
|
|||||||
@@ -38,13 +38,18 @@ public class CoachWidgetBridgePlugin: CAPPlugin, CAPBridgedPlugin {
|
|||||||
}
|
}
|
||||||
var forme: CoachWidgetSnapshot.FormeScore?
|
var forme: CoachWidgetSnapshot.FormeScore?
|
||||||
if let f = call.getObject("forme") {
|
if let f = call.getObject("forme") {
|
||||||
|
// `as? Int` échoue si le JS envoie un float (72.0) — le nombre transite
|
||||||
|
// en NSNumber via le bridge ; `.intValue` couvre Int comme Double.
|
||||||
forme = CoachWidgetSnapshot.FormeScore(
|
forme = CoachWidgetSnapshot.FormeScore(
|
||||||
score: f["score"] as? Int,
|
score: (f["score"] as? NSNumber)?.intValue,
|
||||||
label: f["label"] as? String
|
label: f["label"] as? String
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
let snapshot = CoachWidgetSnapshot(updatedAt: Date(), today: today, forme: forme)
|
let snapshot = CoachWidgetSnapshot(updatedAt: Date(), today: today, forme: forme)
|
||||||
CoachWidgetStore.save(snapshot)
|
guard CoachWidgetStore.save(snapshot) else {
|
||||||
|
call.reject("Échec de sauvegarde du snapshot widget (App Group indisponible ?)")
|
||||||
|
return
|
||||||
|
}
|
||||||
if #available(iOS 14.0, *) { WidgetCenter.shared.reloadAllTimelines() }
|
if #available(iOS 14.0, *) { WidgetCenter.shared.reloadAllTimelines() }
|
||||||
pushToWatch(snapshot)
|
pushToWatch(snapshot)
|
||||||
call.resolve(["ok": true])
|
call.resolve(["ok": true])
|
||||||
|
|||||||
@@ -18,9 +18,13 @@ enum CoachWidgetStore {
|
|||||||
|
|
||||||
static var defaults: UserDefaults? { UserDefaults(suiteName: appGroup) }
|
static var defaults: UserDefaults? { UserDefaults(suiteName: appGroup) }
|
||||||
|
|
||||||
static func save(_ snapshot: CoachWidgetSnapshot) {
|
/// Renvoie `false` si l'App Group est indisponible ou l'encodage échoue —
|
||||||
guard let d = defaults, let data = try? JSONEncoder().encode(snapshot) else { return }
|
/// permet à l'appelant (bridge) de signaler l'échec au lieu de mentir « ok ».
|
||||||
|
@discardableResult
|
||||||
|
static func save(_ snapshot: CoachWidgetSnapshot) -> Bool {
|
||||||
|
guard let d = defaults, let data = try? JSONEncoder().encode(snapshot) else { return false }
|
||||||
d.set(data, forKey: snapshotKey)
|
d.set(data, forKey: snapshotKey)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
static func load() -> CoachWidgetSnapshot? {
|
static func load() -> CoachWidgetSnapshot? {
|
||||||
|
|||||||
Reference in New Issue
Block a user