diff --git a/.gitignore b/.gitignore index da66cc9..9e58663 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.txt # Miscellaneous *.class *.log diff --git a/example/lib/sign_in.dart b/example/lib/sign_in.dart index 214ae1c..407488c 100644 --- a/example/lib/sign_in.dart +++ b/example/lib/sign_in.dart @@ -57,6 +57,11 @@ class SignUp extends StatelessWidget { spacer, SupaSocialsAuth( colored: true, + nativeGoogleAuthConfig: const NativeGoogleAuthConfig( + webClientId: 'YOUR_WEB_CLIENT_ID', + iosClientId: 'YOUR_IOS_CLIENT_ID', + ), + enableNativeAppleAuth: false, socialProviders: OAuthProvider.values, onSuccess: (session) { Navigator.of(context).pushReplacementNamed('/home'); diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 92b6497..464810a 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,13 +6,17 @@ import FlutterMacOS import Foundation import app_links +import google_sign_in_ios import path_provider_foundation import shared_preferences_foundation +import sign_in_with_apple import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/lib/src/components/supa_socials_auth.dart b/lib/src/components/supa_socials_auth.dart index 47bf4ef..4cd96ef 100644 --- a/lib/src/components/supa_socials_auth.dart +++ b/lib/src/components/supa_socials_auth.dart @@ -1,7 +1,13 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:crypto/crypto.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; import 'package:supabase_auth_ui/src/utils/constants.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -20,7 +26,7 @@ extension on OAuthProvider { OAuthProvider.slack => FontAwesomeIcons.slack, OAuthProvider.spotify => FontAwesomeIcons.spotify, OAuthProvider.twitch => FontAwesomeIcons.twitch, - OAuthProvider.twitter => FontAwesomeIcons.x, + OAuthProvider.twitter => FontAwesomeIcons.xTwitter, _ => Icons.close, }; @@ -58,8 +64,31 @@ enum SocialButtonVariant { iconAndText, } +class NativeGoogleAuthConfig { + /// Web Client ID that you registered with Google Cloud. + /// + /// Required to perform native Google Sign In on Android + final String? webClientId; + + /// iOS Client ID that you registered with Google Cloud. + /// + /// Required to perform native Google Sign In on iOS + final String? iosClientId; + + const NativeGoogleAuthConfig({ + this.webClientId, + this.iosClientId, + }); +} + /// UI Component to create social login form class SupaSocialsAuth extends StatefulWidget { + /// Defines native google provider to show in the form + final NativeGoogleAuthConfig? nativeGoogleAuthConfig; + + /// Whether to use native Apple sign in on iOS and macOS + final bool enableNativeAppleAuth; + /// List of social providers to show in the form final List socialProviders; @@ -77,7 +106,7 @@ class SupaSocialsAuth extends StatefulWidget { final String? redirectUrl; /// Method to be called when the auth action is success - final void Function(Session) onSuccess; + final void Function(Session session) onSuccess; /// Method to be called when the auth action threw an excepction final void Function(Object error)? onError; @@ -87,6 +116,8 @@ class SupaSocialsAuth extends StatefulWidget { const SupaSocialsAuth({ Key? key, + this.nativeGoogleAuthConfig, + this.enableNativeAppleAuth = true, required this.socialProviders, this.colored = true, this.redirectUrl, @@ -103,6 +134,63 @@ class SupaSocialsAuth extends StatefulWidget { class _SupaSocialsAuthState extends State { late final StreamSubscription _gotrueSubscription; + /// Performs Google sign in on Android and iOS + Future _nativeGoogleSignIn({ + required String? webClientId, + required String? iosClientId, + }) async { + final GoogleSignIn googleSignIn = GoogleSignIn( + clientId: iosClientId, + serverClientId: webClientId, + ); + + final googleUser = await googleSignIn.signIn(); + final googleAuth = await googleUser!.authentication; + final accessToken = googleAuth.accessToken; + final idToken = googleAuth.idToken; + + if (accessToken == null) { + throw const AuthException( + 'No Access Token found from Google sign in result.'); + } + if (idToken == null) { + throw const AuthException( + 'No ID Token found from Google sign in result.'); + } + + return supabase.auth.signInWithIdToken( + provider: OAuthProvider.google, + idToken: idToken, + accessToken: accessToken, + ); + } + + /// Performs Apple sign in on iOS or macOS + Future _nativeAppleSignIn() async { + final rawNonce = supabase.auth.generateRawNonce(); + final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString(); + + final credential = await SignInWithApple.getAppleIDCredential( + scopes: [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName, + ], + nonce: hashedNonce, + ); + + final idToken = credential.identityToken; + if (idToken == null) { + throw const AuthException( + 'Could not find ID Token from generated Apple sign in credential.'); + } + + return supabase.auth.signInWithIdToken( + provider: OAuthProvider.apple, + idToken: idToken, + nonce: rawNonce, + ); + } + @override void initState() { super.initState(); @@ -127,6 +215,8 @@ class _SupaSocialsAuthState extends State { @override Widget build(BuildContext context) { final providers = widget.socialProviders; + final googleAuthConfig = widget.nativeGoogleAuthConfig; + final isNativeAppleAuthEnabled = widget.enableNativeAppleAuth; final coloredBg = widget.colored == true; if (providers.isEmpty) { @@ -200,12 +290,38 @@ class _SupaSocialsAuthState extends State { ); break; default: - // Handle other cases or provide a default behavior. break; } onAuthButtonPressed() async { try { + // Check if native Google login should be performed + if (socialProvider == OAuthProvider.google) { + final webClientId = googleAuthConfig?.webClientId; + final iosClientId = googleAuthConfig?.iosClientId; + final shouldPerformNativeGoogleSignIn = + (webClientId != null && !kIsWeb && Platform.isAndroid) || + (iosClientId != null && !kIsWeb && Platform.isIOS); + if (shouldPerformNativeGoogleSignIn) { + await _nativeGoogleSignIn( + webClientId: webClientId, + iosClientId: iosClientId, + ); + return; + } + } + + // Check if native Apple login should be performed + if (socialProvider == OAuthProvider.apple) { + final shouldPerformNativeAppleSignIn = + (isNativeAppleAuthEnabled && !kIsWeb && Platform.isIOS) || + (isNativeAppleAuthEnabled && !kIsWeb && Platform.isMacOS); + if (shouldPerformNativeAppleSignIn) { + await _nativeAppleSignIn(); + return; + } + } + await supabase.auth.signInWithOAuth( socialProvider, redirectTo: widget.redirectUrl, diff --git a/pubspec.yaml b/pubspec.yaml index c7df9c7..b87c1fd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,11 +2,11 @@ name: supabase_auth_ui description: UI library to implement auth forms using Supabase and Flutter version: 0.4.0+1 homepage: https://supabase.com -repository: 'https://github.com/supabase-community/flutter-auth-ui' +repository: "https://github.com/supabase-community/flutter-auth-ui" environment: - sdk: '>=3.0.0 <4.0.0' - flutter: '>=3.0.0' + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -14,6 +14,9 @@ dependencies: supabase_flutter: ^2.0.1 email_validator: ^2.0.1 font_awesome_flutter: ^10.6.0 + google_sign_in: ^6.2.1 + sign_in_with_apple: ^5.0.0 + crypto: ^3.0.3 dev_dependencies: flutter_test: