הוספת כניסה לאפליקציית iOS באמצעות FirebaseUI

‫FirebaseUI ל-SwiftUI היא ספרייה מודרנית שמתמקדת ב-SwiftUI, מבוססת על Firebase Authentication ומספקת תהליכי כניסה מוכנים מראש לאפליקציה.

אלה היתרונות של FirebaseUI ל-SwiftUI:

  • ממשק משתמש מוגדר מראש כברירת מחדל: הוספת תהליך כניסה מלא באמצעות AuthPickerView.
  • ניתן להתאמה אישית: אפשר להציג את לחצני ברירת המחדל בפריסה משלכם, או ליצור חוויה בהתאמה אישית מלאה.
  • קישור חשבונות אנונימיים: אפשר לשדרג משתמשים לא רשומים במקום להחליף אותם.
  • ניהול חשבונות: תהליכים מובנים להרשמה, שחזור סיסמה וניהול חשבונות.
  • ספקים מרובים: אימייל/סיסמה, קישור לאימייל, אימות טלפוני, Apple,‏ Google,‏ Facebook,‏ Twitter וספקי OAuth2/OIDC רגילים.
  • תכונות אימות מודרניות: תמיכה מובנית באימות רב-שלבי (MFA) ובממשקי API של async/await.

לפני שמתחילים

התקנה

‫FirebaseUI ל-SwiftUI מסופק כחבילת Swift. כדי להתקין את החבילה בפרויקט Xcode:

  1. ב-Xcode, לוחצים על File (קובץ) > Add Package Dependencies (הוספת יחסי תלות בין חבילות).

  2. מזינים את כתובת ה-URL של החבילה:

    https://github.com/firebase/FirebaseUI-iOS
    
  3. בתפריט Dependency Rule, בוחרים באפשרות Up to Next Major Version ומגדירים את הגרסה המינימלית לגרסה האחרונה.

  4. לוחצים על Add Package (הוספת חבילה) ובוחרים את הספריות שרוצים להוסיף לפרויקט.

    הספרייה הבאה תמיד נדרשת. הוא כולל את יחסי התלות העיקריים וגם תמיכה בכניסה באמצעות כתובת אימייל וסיסמה ובכניסה באמצעות קישור באימייל.

    • FirebaseAuthSwiftUI

     אם רוצים לתמוך בשיטות כניסה אחרות, צריך לבחור גם אחת או יותר מהספריות הבאות:

    • ‫FirebaseAppleSwiftUI (כניסה באמצעות חשבון אפל)
    • FirebaseGoogleSwiftUI (כניסה באמצעות חשבון Google)
    • FirebaseFacebookSwiftUI (כניסה באמצעות פייסבוק)
    • FirebasePhoneAuthSwiftUI (אימות באמצעות הטלפון)
    • FirebaseTwitterSwiftUI (כניסה באמצעות Twitter)
    • FirebaseOAuthSwiftUI (ספקי OAuth ו-OIDC רגילים כמו GitHub, ‏Microsoft ו-Yahoo)
  5. לוחצים על Add Package כדי להתקין את הספריות שנבחרו.

  6. מגדירים את AuthService של FirebaseUI במאתחל של View ברמה העליונה ומעבירים את השירות לתצוגות הצאצא באמצעות הסביבה.

    import FirebaseAuthSwiftUI
    import SwiftUI
    
    struct ContentView: View {
      let authService: AuthService
    
      init() {
        let configuration = AuthConfiguration()
    
        authService = AuthService(configuration: configuration)
          .withEmailSignIn() // Or whatever sign-in methods you want to support.
                             // See the next section.
      }
    
      var body: some View {
        AuthPickerView { // AuthPickerView (the prebuilt View) or a custom View.
            Text("Welcome to your app!")
        }
        .environment(authService)
      }
    }
    

הגדרת שיטות כניסה לחשבון

כל אחת משיטות הכניסה הנתמכות דורשת כמה שלבי הגדרה נוספים. כדי להגדיר ספקים ספציפיים, מרחיבים את הקטעים הבאים ופועלים לפי ההוראות.

כתובת אימייל וסיסמה

  1. בקטע Security > Authentication > שיטת התחברות במסוף Firebase, מפעילים את הספק Email/Password.

  2. רושמים את הספק במכונה של AuthService:

    let authService = AuthService()
      .withEmailSignIn()
    

כדי להיכנס לחשבון באמצעות קישור באימייל בלי סיסמה:

  1. בקטע Security > Authentication > שיטת התחברות במסוף Firebase, מפעילים את האפשרות Email/Password ואז מפעילים את האפשרות Email link sign-in. שימו לב שכדי להשתמש בכניסה באמצעות קישור באימייל, צריך להפעיל את הכניסה באמצעות אימייל או סיסמה.

  2. מוסיפים את הדומיין של הקישור לדומיינים מורשים.

  3. מגדירים את ActionCodeSettings, מעבירים אותו אל AuthConfiguration ורושמים את הספק:

    let actionCodeSettings = ActionCodeSettings()
    actionCodeSettings.handleCodeInApp = true
    actionCodeSettings.url = URL(string: "https://yourapp.firebaseapp.com")
    
    guard let bundleID = Bundle.main.bundleIdentifier else {
      fatalError("Missing bundle identifier for email link authentication setup.")
    }
    actionCodeSettings.setIOSBundleID(bundleID)
    
    let configuration = AuthConfiguration(
      emailLinkSignInActionCodeSettings: actionCodeSettings
    )
    
    let authService = AuthService(configuration: configuration)
      .withEmailLinkSignIn()
    
  4. באותו AppDelegate שבו קוראים ל-FirebaseApp.configure(), מוסיפים לוגיקה ל-method‏ application(_:open:options:) שמחזירה את true כשכתובת ה-URL שנפתחת היא קישור לכניסה ל-Firebase Authentication. הלוגיקה הזו מציינת שהקישור טופל על ידי FirebaseUI, ואין לעבד אותו על ידי רכיבים אחרים לטיפול בקישורים.

    import FacebookCore
    import FirebaseAuth
    import UIKit
    
    class AppDelegate: NSObject, UIApplicationDelegate {
      func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
      ) -> Bool {
        FirebaseApp.configure()
        return true
      }
    
      func application(
        _ application: UIApplication,
        open url: URL,
        options: [UIApplication.OpenURLOptionsKey: Any] = [:]
      ) -> Bool {
        if Auth.auth().canHandle(url) {
          return true
        }
        return false
      }
    }
    
  5. אם אתם יוצרים תצוגות בהתאמה אישית, צריך להפעיל את authService.handleSignInLink(url:) כשהקישור פותח את האפליקציה.

Apple

כדי להשתמש ב"כניסה באמצעות חשבון Apple":

  1. הגדרת "כניסה באמצעות חשבון Apple":

    1. מפעילים את האפשרות "כניסה באמצעות חשבון אפל" באפליקציה בדף Certificates, Identifiers & Profiles (אישורים, מזהים ופרופילים) באתר המפתחים של אפל.
    2. משייכים את האתר לאפליקציה, כמו שמתואר בקטע הראשון במאמר Configure Sign In with Apple for the web (הגדרת כניסה באמצעות אפל לאתר). כשמוצגת בקשה, רושמים את כתובת ה-URL הבאה ככתובת URL להחזרה:
      https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler
      אפשר למצוא את מזהה הפרויקט ב-Firebase בדף ההגדרות של מסוף Firebase. בסיום, שימו לב למזהה השירות החדש, שתצטרכו להשתמש בו בקטע הבא.
    3. יצירת מפתח פרטי לכניסה באמצעות חשבון Apple. בקטע הבא תצטרכו את המפתח הפרטי החדש ואת מזהה המפתח.
    4. אם אתם משתמשים בתכונות של Firebase Authentication ששולחות אימיילים למשתמשים, כולל כניסה באמצעות קישור באימייל, אימות כתובת אימייל, ביטול שינויים בחשבון ועוד, הגדירו את שירות ממסר האימייל הפרטי של אפל ורשמו את noreply@YOUR_FIREBASE_PROJECT_ID.firebaseapp.com (או את הדומיין של תבנית האימייל המותאמת אישית שלכם) כדי שאפל תוכל להעביר אימיילים שנשלחו על ידי Firebase Authentication לכתובות אימייל אנונימיות של אפל.
  2. בקטע Security > Authentication > שיטת התחברות במסוף Firebase, מפעילים את Apple.

    • מציינים את מזהה השירות שיצרתם בקטע הקודם.
    • בקטע OAuth code flow configuration, מציינים את מזהה הצוות של אפל, את המפתח הפרטי ואת מזהה המפתח שיצרתם בקטע הקודם.
  3. ב-Xcode, פותחים את הקטע Signing & Capabilities (חתימה ויכולות) בכלי לעריכת פרויקטים ומוסיפים את היכולת Sign in with Apple (כניסה באמצעות חשבון אפל).

  4. רושמים את הספק במכונה של AuthService:

    let authService = AuthService()
      .withAppleSignIn()
    

Google

כדי להשתמש בכניסה באמצעות חשבון Google:

  1. במסוף Firebase, בקטע Security > Authentication > שיטת התחברות, מפעילים את הספק Google.

  2. מורידים עותק חדש של הקובץ GoogleService-Info.plist של הפרויקט ומעתיקים אותו לפרויקט ב-Xcode. מחליפים את הגרסאות הקיימות בגרסה החדשה.

  3. מוסיפים סכימות של כתובות URL בהתאמה אישית לפרויקט ב-Xcode:

    1. פותחים את הגדרות הפרויקט: לוחצים על שם הפרויקט בתצוגת העץ שמימין. בקטע TARGETS, בוחרים את האפליקציה, לוחצים על הכרטיסייה Info ומרחיבים את הקטע URL Types.

    2. לוחצים על הלחצן + ומוסיפים סכמת URL למזהה הלקוח ההפוך. כדי למצוא את הערך הזה, פותחים את קובץ התצורה GoogleService-Info.plist ומחפשים את המפתח REVERSED_CLIENT_ID. מעתיקים את הערך של המפתח הזה ומדביקים אותו בתיבה URL Schemes (סכמות של כתובות URL) בדף ההגדרות האישיות. משאירים את שאר השדות כמו שהם.

      בסיום, קובץ התצורה אמור להיראות בערך כך (אבל עם הערכים הספציפיים לאפליקציה שלכם):

  4. רושמים את הספק במכונה של AuthService:

    let authService = AuthService()
      .withGoogleSignIn()
    

Facebook

כדי להשתמש בהתחברות באמצעות פייסבוק:

  1. מגדירים את Facebook Login for iOS SDK לפי ההוראות באתר Meta for Developers. מדלגים על השלב האחרון, "Add Facebook Login to Your Code" (הוספת התחברות באמצעות פייסבוק לקוד).

  2. בקטע Security > Authentication > שיטת התחברות במסוף Firebase, מפעילים את הספק פייסבוק. תצטרכו את מזהה האפליקציה בפייסבוק ואת הסוד של האפליקציה מהאתר Meta for Developers.

  3. רושמים את הספק במכונה של AuthService:

    let authService = AuthService()
     .withFacebookSignIn()
    

מספר טלפון

כדי להשתמש באימות באמצעות הטלפון:

  1. בקטע Security > Authentication > שיטת התחברות במסוף Firebase, מפעילים את הספק Phone.

  2. מגדירים את APNs לאפליקציה לפי ההוראות בקטע התחלת קבלת התראות שקטות.

  3. מוסיפים סכימות של כתובות URL בהתאמה אישית לפרויקט ב-Xcode:

    1. פותחים את הגדרות הפרויקט: לוחצים על שם הפרויקט בתצוגת העץ שמימין. בקטע TARGETS, בוחרים את האפליקציה, לוחצים על הכרטיסייה Info ומרחיבים את הקטע URL Types.

    2. לוחצים על הלחצן + ומוסיפים את מזהה האפליקציה המקודד כסכמת URL. כדי למצוא את הערך הזה, פותחים את Settings > General במסוף Firebase.

      בסיום, קובץ התצורה אמור להיראות בערך כך (אבל עם הערכים הספציפיים לאפליקציה שלכם):

  4. באותו AppDelegate שבו קוראים ל-FirebaseApp.configure(), מוסיפים פונקציות טיפול בטוקנים של APNs:

    import FacebookCore
    import FirebaseAuth
    import UIKit
    
    class AppDelegate: NSObject, UIApplicationDelegate {
      func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
      ) -> Bool {
        FirebaseApp.configure()
        return true
      }
    
      func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
      ) {
        #if DEBUG
        Auth.auth().setAPNSToken(deviceToken, type: .sandbox)
        #else
        Auth.auth().setAPNSToken(deviceToken, type: .prod)
        #endif
      }
    }
    
  5. רושמים את הספק במכונה של AuthService:

    let authService = AuthService()
      .withPhoneSignIn()
    

‫Twitter‏ (X)

כדי להשתמש ב-Twitter Login:

  1. יוצרים פרטי כניסה ל-X API לפי ההוראות באתר X Developers.

  2. במסוף Firebase, בקטע Security > Authentication > שיטת התחברות, מפעילים את הספק Twitter. תצטרכו את מפתח ה-API ואת API Secret מ-X Developer Console.

  3. רושמים את הספק במכונה של AuthService:

    let authService = AuthService()
      .withTwitterSignIn()
    

ספקי OAuth2 ו-OIDC רגילים

‫FirebaseUI תומך גם בספקי OAuth מובנים כמו GitHub,‏ Microsoft ו-Yahoo, וגם בספקי OIDC בהתאמה אישית שמוגדרים באימות ב-Firebase.

 let authService = AuthService()
  .withOAuthSignIn(OAuthProviderSwift.github())
  .withOAuthSignIn(OAuthProviderSwift.microsoft())
  .withOAuthSignIn(OAuthProviderSwift.yahoo())

אם אתם משתמשים בספקי OIDC בהתאמה אישית, קודם צריך להגדיר את הספק ב-Firebase Authentication, ואז ליצור OAuthProviderSwift עם מזהה הספק והגדרת הלחצן:

let lineProvider = OAuthProviderSwift(
  providerId: "oidc.line",
  buttonLabel: "Sign in with LINE",
  displayName: "LINE",
  iconSystemName: "person.crop.circle.badge.checkmark",
  buttonBackgroundColor: .green,
  buttonForegroundColor: .white
)

let authService = AuthService()
  .withOAuthSignIn(lineProvider)

שימוש בתצוגת האימות המובנית

‫FirebaseUI ל-SwiftUI מספק את AuthPickerView, ממשק משתמש מובנה לאימות שמטפל בכל תהליך האימות בשבילכם. זו הדרך הכי קלה להוסיף אימות לאפליקציה.

דוגמה

דוגמה לשימוש ב-AuthPickerView עם כמה ספקים ואפשרויות הגדרה:

import FirebaseAppleSwiftUI
import FirebaseAuthSwiftUI
import FirebaseGoogleSwiftUI
import SwiftUI

struct ContentView: View {
  let authService: AuthService

  init() {
    // Create configuration with options
    let configuration = AuthConfiguration(
      tosUrl: URL(string: "https://example.com/tos"),
      privacyPolicyUrl: URL(string: "https://example.com/privacy"),
      shouldAutoUpgradeAnonymousUsers: true
    )

    // Initialize AuthService with multiple providers
    authService = AuthService(configuration: configuration)
      .withEmailSignIn()
      .withAppleSignIn()
      .withGoogleSignIn()
  }

  var body: some View {
    AuthPickerView {
      authenticatedContent
    }
    .environment(authService)
  }

  var authenticatedContent: some View {
    NavigationStack {
      VStack(spacing: 20) {
        if authService.authenticationState == .authenticated {
          Text("Authenticated")
          
          Button("Manage Account") {
            authService.isPresented = true
          }
          .buttonStyle(.bordered)
          
          Button("Sign Out") {
            Task {
              try? await authService.signOut()
            }
          }
          .buttonStyle(.borderedProminent)
        } else {
          Text("Not Authenticated")
          
          Button("Sign In") {
            authService.isPresented = true
          }
          .buttonStyle(.borderedProminent)
        }
      }
      .navigationTitle("My App")
    }
    .onChange(of: authService.authenticationState) { _, newValue in
      // Automatically show auth UI when not authenticated
      if newValue != .authenticating {
        authService.isPresented = (newValue == .unauthenticated)
      }
    }
  }
}

התאמה אישית אופיינית

כל הפרמטרים של AuthConfiguration הם אופציונליים, אבל ברוב האפליקציות מתאימים אישית לפחות את ההגדרות הבאות:

let configuration = AuthConfiguration(
    logo: ImageResource.exampleLogoAsset,
    customStringsBundle: .main,
    tosUrl: URL(string: "https://example.com/tos"),
    privacyPolicyUrl: URL(string: "https://example.com/privacy"),
)
  • ‫logo: תמונת הלוגו שמוצגת בגיליון האימות. במאמר הוספת תמונות לפרויקט Xcode מוסבר איך להוסיף נכס תמונות משלכם.

  • ‫customStringsBundle: שימוש במחרוזות בהתאמה אישית מהחבילה שצוינה. הוא משמש ללוקליזציה, אבל גם להתאמה אישית של מחרוזות ברירת המחדל שמשמשות את AuthPickerView. לדוגמה, כדי להגדיר את ההודעה שמוצגת בחלק העליון של גיליון האימות, יוצרים את Localizable.strings עם התוכן:

    "Sign in with Firebase" = "Sign in to use ExampleApp";
    

מה כלול בתצוגה המוגדרת מראש

כשמשתמשים ב-AuthPickerView, מקבלים:

  1. הצגת גיליון: ממשק המשתמש של האימות מופיע כגיליון מודאלי.
  2. ניווט מובנה: ניווט אוטומטי בין מסכי הכניסה, שחזור הסיסמה, האימות הרב-שלבי, הקישור באימייל והאימות הטלפוני
  3. ניהול מצב האימות: מעבר אוטומטי בין ממשק המשתמש של האימות לבין התוכן שלכם על סמך authService.authenticationState
  4. שליטה באמצעות isPresented: אפשר לקבוע מתי גיליון האימות יופיע על ידי הגדרת authService.isPresented = true/false

התנהגויות דעתניות

ההגדרה שמוגדרת כברירת מחדל ב-AuthPickerView מבוססת על דעה מסוימת לגבי הטיפול בכמה תרחישים מורכבים:

1. פתרון בעיות שקשורות לחשבון

כשמתרחשת התנגשות בין חשבונות (למשל, כשנכנסים עם פרטי כניסה שכבר מקושרים לחשבון אחר), AuthPickerView מטפל בה באופן אוטומטי:

  • התנגשויות בשדרוג אנונימי: אם shouldAutoUpgradeAnonymousUsers מופעל ומתרחשת התנגשות במהלך שדרוג אנונימי, המערכת מוציאה באופן אוטומטי את המשתמש הלא רשום ונכנסת עם פרטי הכניסה החדשים.
  • התנגשויות אחרות: אם יש התנגשויות בין פרטי כניסה של חשבונות לא אנונימיים, המערכת שומרת את פרטי הכניסה בהמתנה ומנסה לקשר אותם אחרי כניסה מוצלחת.

הפעולה הזו מטופלת על ידי AccountConflictModifier שמוחל ברמת NavigationStack.

2. אימות רב-שלבי (MFA)

כשהאימות הרב-שלבי מופעל בהגדרות:

  • מזהה באופן אוטומטי מתי נדרש אימות רב-שלבי (MFA) במהלך הכניסה
  • מציג מסכי פתרון מתאימים לאימות רב-שלבי (SMS או TOTP)
  • מטפל בתהליכי ההרשמה והניהול של MFA
  • תמיכה בגורמי אימות מבוססי-SMS ובסיסמאות חד-פעמיות מבוססות-זמן (TOTP)
3. טיפול בשגיאות

התצוגות שמוגדרות כברירת מחדל כוללות טיפול מובנה בשגיאות:

  • הצגת הודעות שגיאה ידידותיות למשתמש בתיבות דו-שיח של התראות
  • מסנן באופן אוטומטי שגיאות שמטופלות באופן פנימי (למשל, שגיאות ביטול, התנגשויות שמטופלות באופן אוטומטי)
  • שימוש בהודעות שגיאה מותאמות לשוק המקומי באמצעות StringUtils
  • השגיאות מופצות דרך מפתח הסביבה reportError

כשמגדירים כניסה באמצעות קישור באימייל:

  • כתובת האימייל נשמרת באופן אוטומטי באחסון של האפליקציה
  • מטפל בניווט בקישורי עומק מאימייל
  • ניהול התהליך המלא של אימות האימייל
  • תמיכה בשדרוג של משתמשים לא רשומים באמצעות קישור באימייל
5. שדרוג אוטומטי של משתמשים לא רשומים

כשההגדרה shouldAutoUpgradeAnonymousUsers מופעלת:

  • ניסיון אוטומטי לקשר חשבונות אנונימיים עם פרטי כניסה חדשים
  • שדרוג סשנים אנונימיים במקום החלפה שלהם, כדי לשמור על נתוני המשתמשים
  • טיפול חלק בהתנגשויות שדרוג
6. אימות מחדש בתצוגות ברירת המחדל

פעולות רגישות כמו מחיקת חשבונות, עדכון סיסמאות או ביטול הרשמה של גורמי MFA דורשות אימות שבוצע לאחרונה. כשמשתמשים בתצוגות ברירת המחדל, האימות מחדש מתבצע באופן אוטומטי על סמך ספק הכניסה של המשתמש.

כשפעולה רגישה דורשת אימות מחדש, התצוגות שמוגדרות כברירת מחדל באופן אוטומטי:

  • ספקי OAuth (כמו Google,‏ Apple,‏ Facebook,‏ Twitter וכו'): להציג התראה שמבקשת מהמשתמש לאשר, ואז לקבל באופן אוטומטי פרטי כניסה חדשים ולהשלים את הפעולה.

  • אימייל/סיסמה: הצגת גיליון שמבקש מהמשתמש להזין את הסיסמה שלו לפני שהוא ממשיך.

  • קישור באימייל: הצגת התראה עם בקשה לשלוח הודעת אימות, ואז הצגת גיליון עם הוראות לבדיקת האימייל. המשתמש לוחץ על הקישור באימייל כדי להשלים את האימות מחדש.

  • טלפון: הצגת התראה שמסבירה שנדרש אימות, ואז הצגת גיליון לאימות קוד ב-SMS.

הפעולה תתבצע שוב באופן אוטומטי אחרי אימות מחדש מוצלח. לא נדרש קוד נוסף כשמשתמשים ב-AuthPickerView או בתצוגות המובנות של ניהול החשבון (UpdatePasswordView,‏ SignedInView וכו').

מתקדם: יצירת תצוגות אימות בהתאמה אישית

אם אתם צריכים יותר שליטה בממשק המשתמש או בתהליך הניווט, אתם יכולים ליצור תצוגות אימות בהתאמה אישית, ועדיין להשתמש ב-AuthService ללוגיקת האימות.

יש הרבה דרכים לשלב רכיבים של FirebaseUI עם לוגיקה מותאמת אישית. בקטעים הבאים מפורטות דוגמאות לכמה מהגישות להתאמה אישית שבהן אפשר להשתמש.

גישה 1: כפתורים בהתאמה אישית עם registerProvider()

כדי לקבל שליטה מלאה על מראה הכפתור, אתם יכולים ליצור הטמעה מותאמת אישית של AuthProviderUI שעוטפת כל ספק ומחזירה את תצוגת הכפתור בהתאמה אישית.

יצירת ממשק משתמש מותאם אישית של ספק

הנה דוגמה ליצירת כפתור מותאם אישית של Twitter:

import FirebaseAuthSwiftUI
import FirebaseTwitterSwiftUI
import SwiftUI

// Step 1: Create your custom button view
struct CustomTwitterButton: View {
  let provider: TwitterProviderSwift
  @Environment(AuthService.self) private var authService
  @Environment(\.mfaHandler) private var mfaHandler

  var body: some View {
    Button {
      Task {
        do {
          let outcome = try await authService.signIn(provider)

          // Handle MFA if required
          if case let .mfaRequired(mfaInfo) = outcome,
             let onMFA = mfaHandler {
            onMFA(mfaInfo)
          }
        } catch {
          // Do Something Else
        }
      }
    } label: {
      HStack { // Your custom icon
        Text("Sign in with Twitter")
          .fontWeight(.semibold)
      }
      .frame(maxWidth: .infinity)
      .padding()
      .background(
        LinearGradient(
          colors: [Color.blue, Color.cyan],
          startPoint: .leading,
          endPoint: .trailing
        )
      )
      .foregroundColor(.white)
      .cornerRadius(12)
      .shadow(radius: 4)
    }
  }
}

// Step 2: Create a custom AuthProviderUI wrapper
class CustomTwitterProviderAuthUI: AuthProviderUI {
  private let typedProvider: TwitterProviderSwift
  var provider: AuthProviderSwift { typedProvider }
  let id: String = "twitter.com"

  init(provider: TwitterProviderSwift = TwitterProviderSwift()) {
    typedProvider = provider
  }

  @MainActor func authButton() -> AnyView {
    AnyView(CustomTwitterButton(provider: typedProvider))
  }
}

// Step 3: Use it in your app
struct ContentView: View {
  let authService: AuthService

  init() {
    let configuration = AuthConfiguration()
    authService = AuthService(configuration: configuration)

    // Register your custom provider UI
    authService.registerProvider(
      providerWithButton: CustomTwitterProviderAuthUI()
    )
    authService.isPresented = true
  }

  var body: some View {
    AuthPickerView {
      usersApp
    }
    .environment(authService)
  }

  var usersApp: some View {
    NavigationStack {
      VStack {
        Button {
          authService.isPresented = true
        } label: {
          Text("Authenticate")
        }
      }
    }
  }
}

דוגמה פשוטה לכפתור בהתאמה אישית

אתם יכולים גם ליצור כפתורים פשוטים יותר בהתאמה אישית לכל ספק:

import FirebaseAuthSwiftUI
import FirebaseGoogleSwiftUI
import FirebaseAppleSwiftUI
import SwiftUI

// Custom Google Provider UI
class CustomGoogleProviderAuthUI: AuthProviderUI {
  private let typedProvider: GoogleProviderSwift
  var provider: AuthProviderSwift { typedProvider }
  let id: String = "google.com"
  
  init() {
    typedProvider = GoogleProviderSwift()
  }
  
  @MainActor func authButton() -> AnyView {
    AnyView(CustomGoogleButton(provider: typedProvider))
  }
}

struct CustomGoogleButton: View {
  let provider: GoogleProviderSwift
  @Environment(AuthService.self) private var authService
  
  var body: some View {
    Button {
      Task {
        try? await authService.signIn(provider)
      }
    } label: {
      HStack {
        Image(systemName: "g.circle.fill")
        Text("My Custom Google Button")
      }
      .frame(maxWidth: .infinity)
      .padding()
      .background(Color.purple) // Your custom color
      .foregroundColor(.white)
      .cornerRadius(10)
    }
  }
}

// Then use it
struct ContentView: View {
  let authService: AuthService
  
  init() {
    let configuration = AuthConfiguration()
    authService = AuthService(configuration: configuration)
      .withAppleSignIn() // Use default Apple button
    
    // Use custom Google button
    authService.registerProvider(
      providerWithButton: CustomGoogleProviderAuthUI()
    )
  }
  
  var body: some View {
    AuthPickerView {
      Text("App Content")
    }
    .environment(authService)
  }
}

הגישה הזו פועלת עם כל הספקים: Google,‏ Apple,‏ Twitter,‏ Facebook,‏ Phone וספקי OAuth. פשוט יוצרים תצוגת כפתור בהתאמה אישית ועוטפים אותה במחלקה שתואמת ל-AuthProviderUI.

גישה 2: כפתורי ברירת מחדל עם תצוגות בהתאמה אישית

אתם יכולים להשתמש ב-AuthService.renderButtons() ולעקוף את AuthPickerView כדי להציג את לחצני האימות שמוגדרים כברירת מחדל, תוך מתן פריסה וניווט משלכם:

import FirebaseAuthSwiftUI
import FirebaseGoogleSwiftUI
import FirebaseAppleSwiftUI
import SwiftUI

struct CustomAuthView: View {
  @Environment(AuthService.self) private var authService

  var body: some View {
    VStack(spacing: 30) {
      // Your custom logo/branding
      Image("app-logo")
        .resizable()
        .frame(width: 150, height: 150)
      
      Text("Welcome to My App")
        .font(.largeTitle)
        .fontWeight(.bold)
      
      Text("Sign in to continue")
        .font(.subheadline)
        .foregroundStyle(.secondary)
      
      // Render default auth buttons
      authService.renderButtons(spacing: 12)
        .padding()
    }
    .padding()
  }
}

struct ContentView: View {
  init() {
    let configuration = AuthConfiguration()
    
    authService = AuthService(configuration: configuration)
      .withGoogleSignIn()
      .withAppleSignIn()
  }
  
  let authService: AuthService

  var body: some View {
    NavigationStack {
      if authService.authenticationState == .authenticated {
        Text("Authenticated!")
      } else {
        CustomAuthView()
      }
    }
    .environment(authService)
  }
}

גישה 3: תצוגות בהתאמה אישית עם ניווט בהתאמה אישית

כדי לקבל שליטה מלאה על כל התהליך, אתם יכולים לעקוף את AuthPickerView ולבנות מערכת ניווט משלכם:

import FirebaseAuth
import FirebaseAuthSwiftUI
import FirebaseGoogleSwiftUI
import SwiftUI

enum CustomAuthRoute {
  case signIn
  case phoneVerification
  case mfaResolution
}

struct ContentView: View {
  private let authService: AuthService
  @State private var navigationPath: [CustomAuthRoute] = []
  @State private var errorMessage: String?

  init() {
    let configuration = AuthConfiguration()
    self.authService = AuthService(configuration: configuration)
      .withGoogleSignIn()
      .withPhoneSignIn()
  }

  var body: some View {
    NavigationStack(path: $navigationPath) {
      Group {
        if authService.authenticationState == .authenticated {
          authenticatedView
        } else {
          customSignInView
        }
      }
      .navigationDestination(for: CustomAuthRoute.self) { route in
        switch route {
        case .signIn:
          customSignInView
        case .phoneVerification:
          customPhoneVerificationView
        case .mfaResolution:
          customMFAView
        }
      }
    }
    .environment(authService)
    .alert("Error", isPresented: .constant(errorMessage != nil)) {
      Button("OK") {
        errorMessage = nil
      }
    } message: {
      Text(errorMessage ?? "")
    }
  }

  var customSignInView: some View {
    VStack(spacing: 20) {
      Text("Custom Sign In")
        .font(.title)
      
      Button("Sign in with Google") {
        Task {
          do {
            let provider = GoogleProviderSwift(clientID: Auth.auth().app?.options.clientID ?? "")
            let outcome = try await authService.signIn(provider)
            
            // Handle MFA if required
            if case .mfaRequired = outcome {
              navigationPath.append(.mfaResolution)
            }
          } catch {
            errorMessage = error.localizedDescription
          }
        }
      }
      .buttonStyle(.borderedProminent)
      
      Button("Phone Sign In") {
        navigationPath.append(.phoneVerification)
      }
      .buttonStyle(.bordered)
    }
    .padding()
  }

  var customPhoneVerificationView: some View {
    Text("Custom Phone Verification View")
    // Implement your custom phone auth UI here
  }

  var customMFAView: some View {
    Text("Custom MFA Resolution View")
    // Implement your custom MFA UI here
  }

  var authenticatedView: some View {
    VStack(spacing: 20) {
      Text("Welcome!")
      Text("Email: \(authService.currentUser?.email ?? "N/A")")
      
      Button("Sign Out") {
        Task {
          try? await authService.signOut()
        }
      }
      .buttonStyle(.borderedProminent)
    }
  }
}

שיקולים חשובים לגבי תצוגות בהתאמה אישית

כשיוצרים תצוגות בהתאמה אישית, צריך לטפל בכמה דברים בעצמכם ש-AuthPickerView מטפל בהם באופן אוטומטי:

  1. התנגשויות בין חשבונות: אתם יכולים להטמיע אסטרטגיה משלכם לפתרון התנגשויות באמצעות AuthServiceError.accountConflict
  2. טיפול ב-MFA: בודקים את SignInOutcome כדי לראות אם יש .mfaRequired ומטפלים בפתרון ה-MFA באופן ידני
  3. שדרוג משתמשים לא רשומים: טיפול בקישור של חשבונות אנונימיים אם shouldAutoUpgradeAnonymousUsers מופעל
  4. מצב ניווט: ניהול הניווט בין מסכי אימות שונים (אימות טלפוני, שחזור סיסמה וכו')
  5. מצבי טעינה: כדי להציג אינדיקטורים לטעינה במהלך פעולות אימות אסינכרוניות, צריך להשתמש ב-authService.authenticationState
  6. אימות מחדש: טיפול בשגיאות אימות מחדש בפעולות רגישות (ראו אימות מחדש בתצוגות בהתאמה אישית בהמשך)

אימות מחדש בתצוגות בהתאמה אישית

כשיוצרים תצוגות בהתאמה אישית, צריך לטפל באימות מחדש על ידי זיהוי שגיאות ספציפיות והטמעה של תהליך משלכם. פעולות רגישות גורמות לארבעה סוגים של שגיאות אימות מחדש, וכל אחת מהן מכילה מידע על ההקשר.

תבניות הטמעה

ספקי OAuth (כמו Google,‏ Apple,‏ Facebook,‏ Twitter):

מזהים את השגיאה וקוראים ל-reauthenticate(context:), שמטפל באופן אוטומטי בתהליך ה-OAuth:

do {
  try await authService.deleteUser()
} catch let error as AuthServiceError {
  if case .oauthReauthenticationRequired(let context) = error {
    try await authService.reauthenticate(context: context)
    try await authService.deleteUser() // Retry operation
  }
}

אימייל/סיסמה:

מזהים את השגיאה, מבקשים סיסמה, יוצרים פרטי כניסה וקוראים ל-reauthenticate(with:):

do {
  try await authService.updatePassword(to: newPassword)
} catch let error as AuthServiceError {
  if case .emailReauthenticationRequired(let context) = error {
    // Show your password prompt UI
    let password = await promptUserForPassword()
    let credential = EmailAuthProvider.credential(
      withEmail: context.email,
      password: password
    )
    try await authService.reauthenticate(with: credential)
    try await authService.updatePassword(to: newPassword) // Retry
  }
}

טלפון:

מזהים את השגיאה, מאמתים את הטלפון, יוצרים פרטי כניסה וקוראים ל-reauthenticate(with:):

do {
  try await authService.deleteUser()
} catch let error as AuthServiceError {
  if case .phoneReauthenticationRequired(let context) = error {
    // Send verification code
    let verificationId = try await authService.verifyPhoneNumber(
      phoneNumber: context.phoneNumber
    )
    // Show your SMS code input UI
    let code = await promptUserForSMSCode()
    let credential = PhoneAuthProvider.provider().credential(
      withVerificationID: verificationId,
      verificationCode: code
    )
    try await authService.reauthenticate(with: credential)
    try await authService.deleteUser() // Retry
  }
}

קישור לאימייל:

מזהים את השגיאה, שולחים הודעת אימות ומטפלים בכתובת ה-URL הנכנסת:

do {
  try await authService.updatePassword(to: newPassword)
} catch let error as AuthServiceError {
  if case .emailLinkReauthenticationRequired(let context) = error {
    // Send verification email
    try await authService.sendEmailSignInLink(
      email: context.email,
      isReauth: true
    )
    // Show your "Check your email" UI
    await showCheckEmailUI()
    // When user taps the link, it opens your app with a URL
    // Handle it in your URL handler:
    // try await authService.handleSignInLink(url: url)
    // The handleSignInLink method automatically completes reauthentication
    try await authService.updatePassword(to: newPassword) // Retry
  }
}

כל האובייקטים של הקשר האימות מחדש כוללים מאפיין .displayMessage לטקסט שגלוי למשתמשים.

ספקי OAuth בהתאמה אישית

אתם יכולים ליצור ספקי OAuth בהתאמה אישית לשירותים נוספים, מעבר לשירותים המובנים:

⚠️ חשוב: כדי להשתמש בספקי OIDC (‏OpenID Connect), צריך להגדיר אותם בהגדרות האימות של הפרויקט ב-Firebase. במסוף Firebase, עוברים אל אימות ← שיטת התחברות ומוסיפים את ספק ה-OIDC עם פרטי הכניסה הנדרשים (מזהה לקוח, סוד לקוח, כתובת URL של המנפיק). צריך גם לרשום את ה-URI להפניה אוטומטית של OAuth שסופק על ידי Firebase במסוף המפתחים של הספק. הוראות מפורטות להגדרה זמינות במסמכי התיעוד של Firebase OIDC.

import FirebaseAuthSwiftUI
import FirebaseOAuthSwiftUI
import SwiftUI

struct ContentView: View {
  let authService: AuthService

  init() {
    let configuration = AuthConfiguration()
    
    authService = AuthService(configuration: configuration)
      .withOAuthSignIn(
        OAuthProviderSwift(
          providerId: "oidc.line",  // LINE OIDC provider
          scopes: ["profile", "openid", "email"],  // LINE requires these scopes
          displayName: "Sign in with LINE",
          buttonIcon: Image("line-logo"),
          buttonBackgroundColor: .green,
          buttonForegroundColor: .white
        )
      )
      .withOAuthSignIn(
        OAuthProviderSwift(
          providerId: "oidc.custom-provider",
          scopes: ["profile", "openid"],
          displayName: "Sign in with Custom",
          buttonIcon: Image(systemName: "person.circle"),
          buttonBackgroundColor: .purple,
          buttonForegroundColor: .white
        )
      )
  }

  var body: some View {
    AuthPickerView {
      Text("App Content")
    }
    .environment(authService)
  }
}