diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 23466e6..80c4839 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 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 */; }; + 60061E60061E60061E060001 /* CoachGoogleAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60061E60061E60061E060002 /* CoachGoogleAuth.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -117,6 +118,7 @@ EBBF61A35F291A021BA75C60 /* CoachLiveActivityWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachLiveActivityWidget.swift; sourceTree = ""; }; F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachHealthRoute.swift; sourceTree = ""; }; A99E51A99E51A99E51A90002 /* CoachAppleAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachAppleAuth.swift; sourceTree = ""; }; + 60061E60061E60061E060002 /* CoachGoogleAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachGoogleAuth.swift; sourceTree = ""; }; F6BEAEF6F6274D61CAF22636 /* CoachWatchApp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachWatchApp.swift; sourceTree = ""; }; FBFCA969A1B083CD3178D9B2 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ @@ -202,6 +204,7 @@ D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */, F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */, A99E51A99E51A99E51A90002 /* CoachAppleAuth.swift */, + 60061E60061E60061E060002 /* CoachGoogleAuth.swift */, E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */, 504EC30B1FED79650016851F /* Main.storyboard */, 504EC30E1FED79650016851F /* Assets.xcassets */, @@ -400,6 +403,7 @@ D1BD5B1990CF3F2B9C5D0001 /* CoachWorkoutKit.swift in Sources */, F1ABCDEF0123456789AB0001 /* CoachHealthRoute.swift in Sources */, A99E51A99E51A99E51A90001 /* CoachAppleAuth.swift in Sources */, + 60061E60061E60061E060001 /* CoachGoogleAuth.swift in Sources */, 88F28EB12FBB330E00E8306E /* CoachWorkoutObserver.swift in Sources */, E2CE6C2AA1DF4F3CAD6E0001 /* MainViewController.swift in Sources */, 21B242F8E61FB07C476C477C /* CoachLiveBridge.swift in Sources */, diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index 3429e38..e960af6 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -1,5 +1,8 @@ import UIKit import Capacitor +#if canImport(GoogleSignIn) +import GoogleSignIn +#endif @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -51,6 +54,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { 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) } diff --git a/ios/App/App/CoachGoogleAuth.swift b/ios/App/App/CoachGoogleAuth.swift new file mode 100644 index 0000000..4684ffc --- /dev/null +++ b/ios/App/App/CoachGoogleAuth.swift @@ -0,0 +1,76 @@ +// CoachGoogleAuth.swift +// Plugin Capacitor natif : Sign in with Google via le SDK GoogleSignIn-iOS. +// +// Usage côté JS (WebView) : +// const r = await window.Capacitor.Plugins.CoachGoogleAuth.signIn(); +// → { idToken, email?, givenName?, familyName? } +// → reject("canceled") si annulé, sinon reject(message) +// +// L'`idToken` (JWT OIDC Google, aud = client iOS) est POSTé sur /auth/google/web +// côté backend qui le vérifie contre le JWKS Google (GOOGLE_CLIENT_IDS doit +// inclure le client iOS) puis pose le cookie de session. +// +// Prérequis Xcode (cf. HANDOFF) : +// - SDK GoogleSignIn ajouté via SPM (https://github.com/google/GoogleSignIn-iOS) +// - GIDClientID + URL scheme (reversed client id) dans Info.plist +// - AppDelegate.application(_:open:) appelle GIDSignIn.sharedInstance.handle(url) +// +// Tant que le SDK n'est pas bundlé, le plugin compile (garde canImport) mais +// signIn() rejette proprement. +import Foundation +import Capacitor +#if canImport(GoogleSignIn) +import GoogleSignIn +#endif + +@objc(CoachGoogleAuthPlugin) +public class CoachGoogleAuthPlugin: CAPPlugin, CAPBridgedPlugin { + public let identifier = "CoachGoogleAuthPlugin" + public let jsName = "CoachGoogleAuth" + public let pluginMethods: [CAPPluginMethod] = [ + CAPPluginMethod(name: "isAvailable", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "signIn", returnType: CAPPluginReturnPromise), + ] + + @objc func isAvailable(_ call: CAPPluginCall) { + #if canImport(GoogleSignIn) + call.resolve(["available": true]) + #else + call.resolve(["available": false, "reason": "GoogleSignIn SDK not bundled"]) + #endif + } + + @objc func signIn(_ call: CAPPluginCall) { + #if canImport(GoogleSignIn) + DispatchQueue.main.async { + guard let vc = self.bridge?.viewController else { + call.reject("No presenting view controller") + return + } + GIDSignIn.sharedInstance.signIn(withPresenting: vc) { result, error in + if let error = error { + let nsErr = error as NSError + if nsErr.code == GIDSignInError.canceled.rawValue { + call.reject("canceled") + } else { + call.reject("Google sign-in failed: \(error.localizedDescription)") + } + return + } + guard let user = result?.user, + let idToken = user.idToken?.tokenString else { + call.reject("No id token returned by Google") + return + } + var data: [String: Any] = ["idToken": idToken] + if let email = user.profile?.email { data["email"] = email } + if let given = user.profile?.givenName { data["givenName"] = given } + if let family = user.profile?.familyName { data["familyName"] = family } + call.resolve(data) + } + } + #else + call.reject("GoogleSignIn SDK not bundled in this build") + #endif + } +} diff --git a/ios/App/App/MainViewController.swift b/ios/App/App/MainViewController.swift index a39ebcb..24c68c2 100644 --- a/ios/App/App/MainViewController.swift +++ b/ios/App/App/MainViewController.swift @@ -25,6 +25,9 @@ class MainViewController: CAPBridgeViewController { // Plugin natif Sign in with Apple — signIn() natif (AuthenticationServices) // → identity_token POSTé sur /auth/apple/web (cf. CoachAppleAuth.swift). bridge?.registerPluginInstance(CoachAppleAuthPlugin()) + // Plugin natif Sign in with Google — signIn() via SDK GoogleSignIn + // → id_token POSTé sur /auth/google/web (cf. CoachGoogleAuth.swift). + bridge?.registerPluginInstance(CoachGoogleAuthPlugin()) } override func viewDidLoad() {