feat(ios): plugin natif Sign in with Apple (CoachAppleAuth)
- CoachAppleAuth.swift : plugin Capacitor (AuthenticationServices) → signIn()
renvoie { identityToken, user, email?, givenName? } au JS, POSTé sur
/auth/apple/web (aud = bundle id ch.hypnotruck.coach).
- App.entitlements : capability com.apple.developer.applesignin (Default).
- project.pbxproj : enregistre CoachAppleAuth.swift dans la target App.
- AppDelegate : l'injection du cookie bypass n'écrase plus une session
existante (sinon le login natif serait remplacé à chaque lancement).
Côté Xcode : ajouter la capability 'Sign in with Apple' puis build device.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
E2CE6C2AA1DF4F3CAD6E0001 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */; };
|
||||
E547F675B2F61C3475D2A787 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93DA6B121E94B11DF938C90 /* LiveActivityManager.swift */; };
|
||||
F1ABCDEF0123456789AB0001 /* CoachHealthRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */; };
|
||||
A99E51A99E51A99E51A90001 /* CoachAppleAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99E51A99E51A99E51A90002 /* CoachAppleAuth.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -115,6 +116,7 @@
|
||||
E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
|
||||
EBBF61A35F291A021BA75C60 /* CoachLiveActivityWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachLiveActivityWidget.swift; sourceTree = "<group>"; };
|
||||
F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachHealthRoute.swift; sourceTree = "<group>"; };
|
||||
A99E51A99E51A99E51A90002 /* CoachAppleAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachAppleAuth.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>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -199,6 +201,7 @@
|
||||
C0AC4A07811751FB78AA0002 /* CoachAuth.swift */,
|
||||
D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */,
|
||||
F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */,
|
||||
A99E51A99E51A99E51A90002 /* CoachAppleAuth.swift */,
|
||||
E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */,
|
||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||
@@ -396,6 +399,7 @@
|
||||
C0AC4A07811751FB78AA0001 /* CoachAuth.swift in Sources */,
|
||||
D1BD5B1990CF3F2B9C5D0001 /* CoachWorkoutKit.swift in Sources */,
|
||||
F1ABCDEF0123456789AB0001 /* CoachHealthRoute.swift in Sources */,
|
||||
A99E51A99E51A99E51A90001 /* CoachAppleAuth.swift in Sources */,
|
||||
88F28EB12FBB330E00E8306E /* CoachWorkoutObserver.swift in Sources */,
|
||||
E2CE6C2AA1DF4F3CAD6E0001 /* MainViewController.swift in Sources */,
|
||||
21B242F8E61FB07C476C477C /* CoachLiveBridge.swift in Sources */,
|
||||
|
||||
@@ -8,5 +8,9 @@
|
||||
<true/>
|
||||
<key>com.apple.developer.healthkit.background-delivery</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -22,6 +22,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
let token = CoachAuth.kCoachWebToken
|
||||
guard !token.isEmpty, token != "REPLACE_ME" else { return }
|
||||
|
||||
// N'écrase PAS une session existante (ex. login natif Apple = cookie
|
||||
// de session per-user déjà posé). On n'injecte le token bypass que si
|
||||
// aucun cookie coach_web_token n'est présent (1er lancement).
|
||||
if let cookies = HTTPCookieStorage.shared.cookies,
|
||||
cookies.contains(where: { $0.name == "coach_web_token" && $0.domain.contains("coach.hypnotruck.ch") }) {
|
||||
return
|
||||
}
|
||||
|
||||
let oneYear: TimeInterval = 60 * 60 * 24 * 365
|
||||
let properties: [HTTPCookiePropertyKey: Any] = [
|
||||
.name: "coach_web_token",
|
||||
|
||||
101
ios/App/App/CoachAppleAuth.swift
Normal file
101
ios/App/App/CoachAppleAuth.swift
Normal file
@@ -0,0 +1,101 @@
|
||||
// CoachAppleAuth.swift
|
||||
// Plugin Capacitor natif : Sign in with Apple via AuthenticationServices.
|
||||
//
|
||||
// Usage côté JS (WebView) :
|
||||
// const r = await window.Capacitor.Plugins.CoachAppleAuth.signIn();
|
||||
// → { identityToken, user, authorizationCode?, email?, givenName?, familyName? }
|
||||
// → reject("canceled") si l'utilisateur annule, sinon reject(message)
|
||||
//
|
||||
// L'`identityToken` est un JWT signé par Apple dont l'`aud` = bundle id
|
||||
// (ch.hypnotruck.coach). On le POST sur /auth/apple/web côté backend qui le
|
||||
// vérifie contre le JWKS Apple (APPLE_CLIENT_IDS inclut le bundle id) puis
|
||||
// pose le cookie de session. Email/nom ne sont fournis qu'au 1er sign-in.
|
||||
import Foundation
|
||||
import Capacitor
|
||||
import AuthenticationServices
|
||||
|
||||
@objc(CoachAppleAuthPlugin)
|
||||
public class CoachAppleAuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "CoachAppleAuthPlugin"
|
||||
public let jsName = "CoachAppleAuth"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "isAvailable", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "signIn", returnType: CAPPluginReturnPromise),
|
||||
]
|
||||
|
||||
private var pendingCall: CAPPluginCall?
|
||||
|
||||
@objc func isAvailable(_ call: CAPPluginCall) {
|
||||
if #available(iOS 13.0, *) {
|
||||
call.resolve(["available": true])
|
||||
} else {
|
||||
call.resolve(["available": false, "reason": "iOS 13+ required"])
|
||||
}
|
||||
}
|
||||
|
||||
@objc func signIn(_ call: CAPPluginCall) {
|
||||
guard #available(iOS 13.0, *) else {
|
||||
call.reject("Sign in with Apple requires iOS 13+")
|
||||
return
|
||||
}
|
||||
self.pendingCall = call
|
||||
DispatchQueue.main.async {
|
||||
let provider = ASAuthorizationAppleIDProvider()
|
||||
let request = provider.createRequest()
|
||||
request.requestedScopes = [.fullName, .email]
|
||||
let controller = ASAuthorizationController(authorizationRequests: [request])
|
||||
controller.delegate = self
|
||||
controller.presentationContextProvider = self
|
||||
controller.performRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension CoachAppleAuthPlugin: ASAuthorizationControllerDelegate {
|
||||
public func authorizationController(
|
||||
controller: ASAuthorizationController,
|
||||
didCompleteWithAuthorization authorization: ASAuthorization
|
||||
) {
|
||||
guard let cred = authorization.credential as? ASAuthorizationAppleIDCredential,
|
||||
let tokenData = cred.identityToken,
|
||||
let identityToken = String(data: tokenData, encoding: .utf8) else {
|
||||
pendingCall?.reject("No identity token returned by Apple")
|
||||
pendingCall = nil
|
||||
return
|
||||
}
|
||||
var result: [String: Any] = [
|
||||
"identityToken": identityToken,
|
||||
"user": cred.user,
|
||||
]
|
||||
if let codeData = cred.authorizationCode,
|
||||
let code = String(data: codeData, encoding: .utf8) {
|
||||
result["authorizationCode"] = code
|
||||
}
|
||||
if let email = cred.email { result["email"] = email }
|
||||
if let given = cred.fullName?.givenName { result["givenName"] = given }
|
||||
if let family = cred.fullName?.familyName { result["familyName"] = family }
|
||||
pendingCall?.resolve(result)
|
||||
pendingCall = nil
|
||||
}
|
||||
|
||||
public func authorizationController(
|
||||
controller: ASAuthorizationController,
|
||||
didCompleteWithError error: Error
|
||||
) {
|
||||
let nsErr = error as NSError
|
||||
if nsErr.code == ASAuthorizationError.canceled.rawValue {
|
||||
pendingCall?.reject("canceled")
|
||||
} else {
|
||||
pendingCall?.reject("Apple sign-in failed: \(error.localizedDescription)")
|
||||
}
|
||||
pendingCall = nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension CoachAppleAuthPlugin: ASAuthorizationControllerPresentationContextProviding {
|
||||
public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
||||
return self.bridge?.viewController?.view.window ?? ASPresentationAnchor()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user