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 */; };
|
E2CE6C2AA1DF4F3CAD6E0001 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */; };
|
||||||
E547F675B2F61C3475D2A787 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93DA6B121E94B11DF938C90 /* LiveActivityManager.swift */; };
|
E547F675B2F61C3475D2A787 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93DA6B121E94B11DF938C90 /* LiveActivityManager.swift */; };
|
||||||
F1ABCDEF0123456789AB0001 /* CoachHealthRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1ABCDEF0123456789AB0002 /* CoachHealthRoute.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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -115,6 +116,7 @@
|
|||||||
E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
FBFCA969A1B083CD3178D9B2 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
@@ -199,6 +201,7 @@
|
|||||||
C0AC4A07811751FB78AA0002 /* CoachAuth.swift */,
|
C0AC4A07811751FB78AA0002 /* CoachAuth.swift */,
|
||||||
D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */,
|
D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */,
|
||||||
F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */,
|
F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */,
|
||||||
|
A99E51A99E51A99E51A90002 /* CoachAppleAuth.swift */,
|
||||||
E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */,
|
E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */,
|
||||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||||
504EC30E1FED79650016851F /* Assets.xcassets */,
|
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||||
@@ -396,6 +399,7 @@
|
|||||||
C0AC4A07811751FB78AA0001 /* CoachAuth.swift in Sources */,
|
C0AC4A07811751FB78AA0001 /* CoachAuth.swift in Sources */,
|
||||||
D1BD5B1990CF3F2B9C5D0001 /* CoachWorkoutKit.swift in Sources */,
|
D1BD5B1990CF3F2B9C5D0001 /* CoachWorkoutKit.swift in Sources */,
|
||||||
F1ABCDEF0123456789AB0001 /* CoachHealthRoute.swift in Sources */,
|
F1ABCDEF0123456789AB0001 /* CoachHealthRoute.swift in Sources */,
|
||||||
|
A99E51A99E51A99E51A90001 /* CoachAppleAuth.swift in Sources */,
|
||||||
88F28EB12FBB330E00E8306E /* CoachWorkoutObserver.swift in Sources */,
|
88F28EB12FBB330E00E8306E /* CoachWorkoutObserver.swift in Sources */,
|
||||||
E2CE6C2AA1DF4F3CAD6E0001 /* MainViewController.swift in Sources */,
|
E2CE6C2AA1DF4F3CAD6E0001 /* MainViewController.swift in Sources */,
|
||||||
21B242F8E61FB07C476C477C /* CoachLiveBridge.swift in Sources */,
|
21B242F8E61FB07C476C477C /* CoachLiveBridge.swift in Sources */,
|
||||||
|
|||||||
@@ -8,5 +8,9 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.developer.healthkit.background-delivery</key>
|
<key>com.apple.developer.healthkit.background-delivery</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.developer.applesignin</key>
|
||||||
|
<array>
|
||||||
|
<string>Default</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
let token = CoachAuth.kCoachWebToken
|
let token = CoachAuth.kCoachWebToken
|
||||||
guard !token.isEmpty, token != "REPLACE_ME" else { return }
|
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 oneYear: TimeInterval = 60 * 60 * 24 * 365
|
||||||
let properties: [HTTPCookiePropertyKey: Any] = [
|
let properties: [HTTPCookiePropertyKey: Any] = [
|
||||||
.name: "coach_web_token",
|
.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