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

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

היתרונות של FirebaseUI for SwiftUI:

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

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

התקנה

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

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

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

    https://github.com/firebase/FirebaseUI-iOS
    
  3. בתפריט כלל תלות, בוחרים באפשרות עד הגרסה הראשית הבאה ומגדירים את הגרסה המינימלית להגרסה האחרונה.

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

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

    • FirebaseAuthSwiftUI

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

    • FirebaseAppleSwiftUI (כניסה באמצעות Apple)
    • 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 > Sign-in method (אבטחה > אימות > שיטת כניסה) במסוף Firebase, מפעילים את הספק Email/Password (כתובת אימייל/סיסמה).

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

    let authService = AuthService()
      .withEmailSignIn()
    

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

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

  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(), מוסיפים לוגיקה לשיטה 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. מפעילים את התכונה 'כניסה באמצעות Apple' באפליקציה בדף Certificates, Identifiers & Profiles באתר המפתחים של Apple.
    2. משייכים את האתר לאפליקציה כמו שמתואר בקטע הראשון של הגדרת כניסה באמצעות Apple לאינטרנט. כשתופיע בקשה, צריך לרשום את כתובת ה-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 > Sign-in method במסוף Firebase, מפעילים את Apple.

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

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

    let authService = AuthService()
      .withAppleSignIn()
    

Google

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

  1. בקטע Security > Authentication > Sign-in method (אבטחה > אימות > שיטת כניסה) במסוף Firebase, מפעילים את הספק Google.

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

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

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

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

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

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

    let authService = AuthService()
      .withGoogleSignIn()
    

Facebook

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

  1. מגדירים את Facebook Login for iOS SDK לפי ההוראות באתר Meta for Developers. מדלגים על השלב האחרון, 'הוספת התחברות באמצעות פייסבוק לקוד'.

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

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

    let authService = AuthService()
     .withFacebookSignIn()
    

מספר טלפון

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

  1. בקטע Security > Authentication > Sign-in method במסוף Firebase, מפעילים את הספק Phone.

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

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

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

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

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

  4. באותו AppDelegate שבו קוראים ל-FirebaseApp.configure(), מוסיפים את רכיבי ה-handler של טוקן 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:

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

  2. בקטע Security > Authentication > Sign-in method במסוף Firebase, מפעילים את הספק Twitter. תצטרכו את מפתח ה-API ואת סוד ה-API מ-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 for 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. Sheet Presentation: ממשק המשתמש של האימות מופיע כגיליון מודאלי
  2. ניווט מובנה: ניווט אוטומטי בין מסכי הכניסה, שחזור הסיסמה, MFA, קישור לאימייל ואימות הטלפון
  3. ניהול מצב האימות: מעבר אוטומטי בין ממשק המשתמש של האימות לבין התוכן שלכם על סמך authService.authenticationState
  4. שליטה באמצעות isPresented: כדי לקבוע מתי גיליון ההרשאות יופיע, מגדירים את authService.isPresented = true/false

התנהגויות מבוססות-הנחות

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

יצירת ממשק משתמש של ספק בהתאמה אישית

דוגמה ליצירת לחצן מותאם אישית לטוויטר:

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, טלפון וספקי 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 ולטפל בפתרון בעיות שקשורות לאימות רב-שלבי באופן ידני
  3. שדרוגים של משתמשים אנונימיים: טיפול בקישור של חשבונות אנונימיים אם האפשרות shouldAutoUpgradeAnonymousUsers מופעלת
  4. מצב ניווט: ניהול הניווט בין מסכי אימות שונים (אימות טלפון, שחזור סיסמה וכו')
  5. מצבי טעינה: הצגת אינדיקטורים לטעינה במהלך פעולות אימות אסינכרוניות על ידי מעקב אחרי authService.authenticationState
  6. אימות מחדש: טיפול בשגיאות שקשורות לאימות מחדש בפעולות רגישות (ראו אימות מחדש בתצוגות בהתאמה אישית בהמשך)

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

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

דפוסי הטמעה

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

כדי לטפל בתהליך OAuth באופן אוטומטי, צריך לזהות את השגיאה ולהפעיל את reauthenticate(context:):

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 Console, עוברים אל אימות ← שיטת כניסה ומוסיפים את ספק 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)
  }
}