Как добавить в приложение для iOS возможность входа с помощью FirebaseUI

FirebaseUI для SwiftUI – это современная библиотека, созданная на основе Firebase Authentication и позволяющая добавлять в приложение готовые процессы входа.

FirebaseUI для SwiftUI имеет следующие преимущества:

  • Готовый интерфейс по умолчанию. Добавьте полный процесс входа с помощью AuthPickerView.
  • Настраиваемость. Вы можете использовать стандартные кнопки в собственном макете или создать полностью настраиваемый интерфейс.
  • Связывание анонимных аккаунтов. Вы можете не заменять анонимных пользователей, а преобразовывать их в обычные.
  • Управление аккаунтом. Встроенные процессы регистрации, восстановления пароля и управления аккаунтом.
  • Несколько поставщиков: адрес электронной почты и пароль, ссылка в электронном письме, аутентификация по телефону, Apple, Google, Facebook, Twitter и стандартные поставщики OAuth2/OIDC.
  • Современные функции аутентификации. Встроенная поддержка многофакторной аутентификации (МФА) и API async/await.

Подготовка

  • Добавьте Firebase в проект Apple. Обязательно выполните шаги SwiftUI.

  • Убедитесь, что ваше приложение предназначено для iOS 17 или более поздней версии.

Установка

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 (Sign in with Apple)
    • FirebaseGoogleSwiftUI (Вход с аккаунтом Google)
    • FirebaseFacebookSwiftUI (Войти с аккаунтом Facebook)
    • FirebasePhoneAuthSwiftUI (аутентификация по телефону)
    • FirebaseTwitterSwiftUI (Sign in with 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. Включите функцию Sign In with Apple для своего приложения на странице Certificates, Identifiers & Profiles (Сертификаты, идентификаторы и профили) на сайте Apple для разработчиков.
    2. Свяжите сайт с приложением, как описано в первом разделе статьи Configure Sign In with Apple for the web (Как настроить функцию "Войти с Apple" для сайта). Когда появится запрос, зарегистрируйте следующий URL в качестве URL возврата:
      https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler
      Идентификатор проекта Firebase можно найти на странице настроек консоли Firebase. Запишите идентификатор сервиса. Он понадобится вам в следующем разделе.
    3. Создайте закрытый ключ для функции "Войти с Apple". Новый закрытый ключ и его идентификатор понадобятся вам в следующем разделе.
    4. Если вы используете какие-либо функции Firebase Authentication, которые отправляют электронные письма пользователям, включая вход по ссылке в электронном письме, подтверждение адреса электронной почты, отмену изменения аккаунта и другие, настройте службу частной ретрансляции электронной почты Apple и зарегистрируйте noreply@YOUR_FIREBASE_PROJECT_ID.firebaseapp.com (или ваш настроенный домен шаблона электронной почты), чтобы Apple могла ретранслировать электронные письма, отправленные Firebase Authentication, на анонимные адреса электронной почты Apple.
  2. В разделе Security (Безопасность) > Authentication (Аутентификация) > Sign-in method (Способ входа) консоли Firebase включите Apple.

    • Укажите идентификатор сервиса, созданный в предыдущем разделе.
    • В разделе OAuth code flow configuration (Конфигурация потока кода OAuth) укажите идентификатор Apple Team ID, а также закрытый ключ и его идентификатор, созданные в предыдущем разделе.
  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. Откройте конфигурацию проекта, нажав на его название в дереве аккаунта слева. Выберите приложение в разделе TARGETS (ЦЕЛИ), откройте вкладку Info (Информация) и разверните раздел URL Types (Типы URL).

    2. Нажмите кнопку + и добавьте схему URL для обратного идентификатора клиента. Чтобы найти это значение, откройте файл конфигурации GoogleService-Info.plist и найдите ключ REVERSED_CLIENT_ID. Скопируйте значение этого ключа и вставьте его в поле URL Schemes (Схемы URL) на странице конфигурации. Другие поля оставьте без изменений.

      После завершения настройки конфигурация должна выглядеть примерно так (но со значениями, относящимися к вашему приложению):

  4. Зарегистрируйте поставщика в экземпляре AuthService:

    let authService = AuthService()
      .withGoogleSignIn()
    

Facebook

Чтобы использовать вход через Facebook:

  1. Настройте Facebook Login для iOS SDK, следуя инструкциям на сайте Meta for Developers. Пропустите последний шаг "Добавьте вход через Facebook в свой код".

  2. В консоли Firebase в разделе Security (Безопасность) > Authentication (Аутентификация) > Sign-in method (Способ входа) включите поставщика Facebook. Вам понадобятся идентификатор и секретный ключ приложения Facebook, которые можно найти на сайте Meta for Developers.

  3. Зарегистрируйте поставщика в экземпляре AuthService:

    let authService = AuthService()
     .withFacebookSignIn()
    

Номер телефона

Чтобы использовать аутентификацию по телефону:

  1. В разделе Security (Безопасность) > Authentication (Аутентификация) > Sign-in method (Способ входа) консоли Firebase включите поставщика Phone (Телефон).

  2. Настройте APN для приложения, как описано в разделе Начните получать тихие уведомления.

  3. Добавьте специальные схемы URL в проект Xcode:

    1. Откройте конфигурацию проекта, нажав на его название в дереве аккаунта слева. Выберите приложение в разделе TARGETS (ЦЕЛИ), откройте вкладку Info (Информация) и разверните раздел URL Types (Типы URL).

    2. Нажмите кнопку + и добавьте закодированный идентификатор приложения в качестве схемы URL. Чтобы найти это значение, откройте консоль Firebase и выберите Settings (Настройки) > General (Общие).

      После завершения настройки конфигурация должна выглядеть примерно так (но со значениями, относящимися к вашему приложению):

  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()
    

X (Твиттер)

Чтобы использовать вход через Твиттер:

  1. Создайте учетные данные X API, следуя инструкциям на сайте X Developers.

  2. В разделе Security > Authentication > Sign-in method (Безопасность > Аутентификация > Способ входа) консоли Firebase включите поставщика Twitter. Вам понадобятся ключ API и секретный ключ API из консоли X Developer.

  3. Зарегистрируйте поставщика в экземпляре AuthService:

    let authService = AuthService()
      .withTwitterSignIn()
    

Стандартные поставщики OAuth2 и OIDC

FirebaseUI также поддерживает встроенных поставщиков OAuth, таких как GitHub, Microsoft и Yahoo, а также пользовательских поставщиков OIDC, настроенных в Firebase Authentication.

 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 – логотип, который показывается на странице аутентификации. Подробнее о том, как добавить собственный объект изображения…

  • customStringsBundle – использовать специальные строки из указанного пакета. Это используется для локализации, а также для настройки строк по умолчанию, используемых AuthPickerView. Например, чтобы задать сообщение, которое будет показываться в верхней части окна аутентификации, создайте файл Localizable.strings со следующим содержимым:

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

Что входит в стандартное представление

При использовании AuthPickerView вы получаете:

  1. Sheet Presentation – интерфейс аутентификации появляется в виде модального окна.
  2. Встроенная навигация. Автоматическая навигация между экранами входа, восстановления пароля, многофакторной аутентификации, ссылки в электронном письме и подтверждения по телефону.
  3. Управление состоянием аутентификации. Автоматическое переключение между интерфейсом аутентификации и контентом на основе authService.authenticationState.
  4. Управление с помощью isPresented. Вы можете управлять показом окна аутентификации, задав значение authService.isPresented = true/false.

Поведение, основанное на убеждениях

В стандартной версии AuthPickerView есть определенные правила для обработки сложных сценариев:

1. Как устранить конфликт аккаунтов

Если возникает конфликт аккаунтов (например, при входе с учетными данными, которые уже связаны с другим аккаунтом), AuthPickerView автоматически обрабатывает его:

  • Конфликты при анонимном обновлении. Если включен параметр shouldAutoUpgradeAnonymousUsers и во время анонимного обновления возникает конфликт, система автоматически выполняет выход из анонимного аккаунта и вход с новыми учетными данными.
  • Другие конфликты. Если конфликт учетных данных возникает между неанонимными аккаунтами, система сохраняет ожидающие учетные данные и пытается связать их после успешного входа.

Это обрабатывается с помощью AccountConflictModifier, применяемого на уровне NavigationStack.

2. Многофакторная аутентификация (МФА)

Если в вашей конфигурации включена многофакторная аутентификация:

  • Автоматически определяет, когда при входе требуется многофакторная аутентификация.
  • Показывает подходящие экраны для многофакторной аутентификации (SMS или TOTP).
  • Управляет регистрацией и процессами многофакторной аутентификации.
  • Поддерживает факторы на основе SMS и одноразовых кодов (TOTP).
3. Обработка ошибок

В представлениях по умолчанию есть встроенная обработка ошибок:

  • Показывает удобные сообщения об ошибках в диалоговых окнах.
  • Автоматически отфильтровывает ошибки, которые обрабатываются внутри системы (например, ошибки отмены, автоматически обрабатываемые конфликты).
  • Использует локализованные сообщения об ошибках через StringUtils.
  • Ошибки распространяются через ключ среды reportError.

Если настроен вход по ссылке в электронном письме:

  • Автоматически сохраняет адрес электронной почты в хранилище приложения.
  • Обрабатывает переходы по ссылкам на контент из электронных писем.
  • Управляет всем процессом подтверждения адреса электронной почты.
  • Поддерживает обновление анонимных пользователей по ссылке в электронном письме.
5. Автоматическое преобразование анонимных пользователей

Если параметр shouldAutoUpgradeAnonymousUsers включен:

  • Автоматически пытается связать анонимные аккаунты с новыми учетными данными для входа.
  • Сохраняет пользовательские данные, обновляя анонимные сеансы, а не заменяя их.
  • Корректно обрабатывает конфликты при обновлении.
6. Повторная аутентификация в представлениях по умолчанию

Для выполнения действий, связанных с безопасностью, например удаления аккаунтов, обновления паролей или отмены регистрации факторов МФА, требуется недавняя аутентификация. При использовании представлений по умолчанию повторная аутентификация выполняется автоматически в зависимости от поставщика входа пользователя.

Если для выполнения действия, связанного с безопасностью, требуется повторная аутентификация, представления по умолчанию автоматически:

  • Поставщики 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. Обработка многофакторной аутентификации. Проверьте SignInOutcome на наличие .mfaRequired и вручную выполните многофакторную аутентификацию.
  3. Обновление анонимных пользователей. Обработка связывания анонимных аккаунтов, если включен параметр shouldAutoUpgradeAnonymousUsers.
  4. Navigation State. Управление навигацией между разными экранами аутентификации (подтверждение по телефону, восстановление пароля и т. д.).
  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 перейдите в раздел Authentication (Аутентификация) → Sign-in method (Способ входа) и добавьте поставщика 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)
  }
}