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:
Sylvain Bettinelli
2026-07-01 06:37:42 +00:00
parent ec975a9a0d
commit e1adbbb61f
4 changed files with 22 additions and 7 deletions

View File

@@ -38,8 +38,13 @@ public class CoachAppleAuthPlugin: CAPPlugin, CAPBridgedPlugin {
call.reject("Sign in with Apple requires iOS 13+")
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 {
// 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 request = provider.createRequest()
request.requestedScopes = [.fullName, .email]
@@ -83,8 +88,8 @@ extension CoachAppleAuthPlugin: ASAuthorizationControllerDelegate {
controller: ASAuthorizationController,
didCompleteWithError error: Error
) {
let nsErr = error as NSError
if nsErr.code == ASAuthorizationError.canceled.rawValue {
// Vérifie le domaine ET le code (un code 1001 d'un autre domaine annulation).
if let authErr = error as? ASAuthorizationError, authErr.code == .canceled {
pendingCall?.reject("canceled")
} else {
pendingCall?.reject("Apple sign-in failed: \(error.localizedDescription)")

View File

@@ -38,13 +38,18 @@ public class CoachWidgetBridgePlugin: CAPPlugin, CAPBridgedPlugin {
}
var forme: CoachWidgetSnapshot.FormeScore?
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(
score: f["score"] as? Int,
score: (f["score"] as? NSNumber)?.intValue,
label: f["label"] as? String
)
}
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() }
pushToWatch(snapshot)
call.resolve(["ok": true])

View File

@@ -18,9 +18,13 @@ enum CoachWidgetStore {
static var defaults: UserDefaults? { UserDefaults(suiteName: appGroup) }
static func save(_ snapshot: CoachWidgetSnapshot) {
guard let d = defaults, let data = try? JSONEncoder().encode(snapshot) else { return }
/// Renvoie `false` si l'App Group est indisponible ou l'encodage échoue
/// 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)
return true
}
static func load() -> CoachWidgetSnapshot? {