/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import FBSDKGamingServicesKit
import XCTest

final class DependentAsObjectTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  private var defaultImplementationDependent: DefaultImplementationDependent!
  private var customImplementationDependent: CustomImplementationDependent!
  // swiftlint:enable implicitly_unwrapped_optional

  private let defaultDependencies = TestDependencies(value: 14)
  private let customDependencies = TestDependencies(value: 28)

  override func setUp() {
    super.setUp()

    defaultImplementationDependent = DefaultImplementationDependent()
    customImplementationDependent = CustomImplementationDependent()
  }

  override func tearDown() {
    defaultImplementationDependent = nil
    customImplementationDependent = nil

    super.tearDown()
  }

  func testMissingDependencies() {
    XCTAssertThrowsError(
      try defaultImplementationDependent.getDependencies(),
      .missingDependencies
    ) { error in
      XCTAssertEqual(
        String(describing: error),
        "The dependencies for the type 'DefaultImplementationDependent' or an instance of it have not been set",
        .missingDependencies
      )
    }
  }

  func testDefaultDependencies() {
    defaultImplementationDependent.defaultDependencies = defaultDependencies
    XCTAssertEqual(
      try defaultImplementationDependent.getDependencies(),
      defaultDependencies,
      .defaultDependencies
    )
  }

  func testConfiguredDependencies() {
    defaultImplementationDependent.defaultDependencies = defaultDependencies
    defaultImplementationDependent.configuredDependencies = customDependencies
    XCTAssertEqual(
      try defaultImplementationDependent.getDependencies(),
      customDependencies,
      .customDependencies
    )
  }

  func testDefaultSetDependencies() {
    defaultImplementationDependent.setDependencies(customDependencies)
    XCTAssertEqual(
      defaultImplementationDependent.configuredDependencies,
      customDependencies,
      .defaultSetDependenciesImplementation
    )
  }

  func testCustomSetDependencies() {
    customImplementationDependent.setDependencies(customDependencies)
    XCTAssertTrue(customImplementationDependent.wasSetDependenciesCalled, .customSetDependenciesImplementation)
  }

  func testDefaultResetDependencies() {
    defaultImplementationDependent.configuredDependencies = customDependencies
    defaultImplementationDependent.resetDependencies()
    XCTAssertNil(defaultImplementationDependent.configuredDependencies, .defaultResetDependenciesImplementation)
  }

  func testCustomResetDependencies() {
    customImplementationDependent.resetDependencies()
    XCTAssertTrue(customImplementationDependent.wasResetDependenciesCalled, .customResetDependenciesImplementation)
  }

  func testFailedDynamicMemberLookup() {
    XCTAssertNil(defaultImplementationDependent.value, .missingDependencyDynamicMemberLookup)
  }

  func testDynamicMemberLookup() {
    defaultImplementationDependent.defaultDependencies = customDependencies
    XCTAssertEqual(
      defaultImplementationDependent.value,
      customDependencies.value,
      .dynamicMemberLookup
    )
  }
}

// MARK: - Test Types

private struct TestDependencies: Equatable {
  let value: Int
}

private final class DefaultImplementationDependent: DependentAsObject {
  var configuredDependencies: TestDependencies?
  var defaultDependencies: TestDependencies?
}

private final class CustomImplementationDependent: DependentAsObject {
  var configuredDependencies: TestDependencies?
  var defaultDependencies: TestDependencies?

  var wasSetDependenciesCalled = false
  var wasResetDependenciesCalled = false

  func setDependencies(_ dependencies: TestDependencies) {
    wasSetDependenciesCalled = true
  }

  func resetDependencies() {
    wasResetDependenciesCalled = true
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let missingDependencies = """
    Attempting to get the missing dependencies of a dependent throws a missing dependencies error
    """
  static let defaultDependencies = """
    When a dependent's configured dependencies are missing, its default dependencies are provided
    """
  static let customDependencies = "When a dependent has configured dependencies, those dependencies are provided"

  static let defaultSetDependenciesImplementation = """
    A dependent has a default `setDependencies(_:)` implementation that sets its configured dependencies
    """
  static let customSetDependenciesImplementation = """
    A dependent can override the default `setDependencies(_:)` implementation
    """

  static let defaultResetDependenciesImplementation = """
    A dependent has a default `resetDependencies()` implementation that clears its configured dependencies
    """
  static let customResetDependenciesImplementation = """
    A dependent can override the default `resetDependencies()` implementation
    """

  static let missingDependencyDynamicMemberLookup = """
    When a dependent's dependencies are missing, dynamic lookup of a dependency as a property yields a nil value
    """
  static let dynamicMemberLookup = "The discrete dependencies of a dependent can be accessed dynamically as properties"
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import FBSDKGamingServicesKit
import XCTest

final class DependentAsTypeTests: XCTestCase {

  private let defaultDependencies = TestDependencies(value: 14)
  private let customDependencies = TestDependencies(value: 28)

  override func setUp() {
    super.setUp()
    resetDependencies()
  }

  override func tearDown() {
    resetDependencies()
    super.tearDown()
  }

  private func resetDependencies() {
    DefaultImplementationDependent.configuredDependencies = nil
    DefaultImplementationDependent.defaultDependencies = nil

    CustomImplementationDependent.configuredDependencies = nil
    CustomImplementationDependent.wasSetDependenciesCalled = false
    CustomImplementationDependent.wasResetDependenciesCalled = false
  }

  func testMissingDependencies() {
    XCTAssertThrowsError(
      try DefaultImplementationDependent.getDependencies(),
      .missingDependencies
    ) { error in
      XCTAssertEqual(
        String(describing: error),
        "The dependencies for the type 'DefaultImplementationDependent' or an instance of it have not been set",
        .missingDependencies
      )
    }
  }

  func testDefaultDependencies() {
    DefaultImplementationDependent.defaultDependencies = defaultDependencies
    XCTAssertEqual(
      try DefaultImplementationDependent.getDependencies(),
      defaultDependencies,
      .defaultDependencies
    )
  }

  func testConfiguredDependencies() {
    DefaultImplementationDependent.defaultDependencies = defaultDependencies
    DefaultImplementationDependent.configuredDependencies = customDependencies
    XCTAssertEqual(
      try DefaultImplementationDependent.getDependencies(),
      customDependencies,
      .customDependencies
    )
  }

  func testDefaultSetDependencies() {
    DefaultImplementationDependent.setDependencies(customDependencies)
    XCTAssertEqual(
      DefaultImplementationDependent.configuredDependencies,
      customDependencies,
      .defaultSetDependenciesImplementation
    )
  }

  func testCustomSetDependencies() {
    CustomImplementationDependent.setDependencies(customDependencies)
    XCTAssertTrue(CustomImplementationDependent.wasSetDependenciesCalled, .customSetDependenciesImplementation)
  }

  func testDefaultResetDependencies() {
    DefaultImplementationDependent.configuredDependencies = customDependencies
    DefaultImplementationDependent.resetDependencies()
    XCTAssertNil(DefaultImplementationDependent.configuredDependencies, .defaultResetDependenciesImplementation)
  }

  func testCustomResetDependencies() {
    CustomImplementationDependent.resetDependencies()
    XCTAssertTrue(CustomImplementationDependent.wasResetDependenciesCalled, .customResetDependenciesImplementation)
  }

  func testFailedDynamicMemberLookup() {
    XCTAssertNil(DefaultImplementationDependent.value, .missingDependencyDynamicMemberLookup)
  }

  func testDynamicMemberLookup() {
    DefaultImplementationDependent.defaultDependencies = customDependencies
    XCTAssertEqual(
      DefaultImplementationDependent.value,
      customDependencies.value,
      .dynamicMemberLookup
    )
  }
}

// MARK: - Test Types

private struct TestDependencies: Equatable {
  let value: Int
}

private enum DefaultImplementationDependent: DependentAsType {
  static var configuredDependencies: TestDependencies?
  static var defaultDependencies: TestDependencies?
}

private enum CustomImplementationDependent: DependentAsType {
  static var configuredDependencies: TestDependencies?
  static var defaultDependencies: TestDependencies?

  static var wasSetDependenciesCalled = false
  static var wasResetDependenciesCalled = false

  static func setDependencies(_ dependencies: TestDependencies) {
    wasSetDependenciesCalled = true
  }

  static func resetDependencies() {
    wasResetDependenciesCalled = true
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let missingDependencies = """
    Attempting to get the missing dependencies of a dependent throws a missing type dependencies error
    """
  static let defaultDependencies = """
    When a dependent's configured dependencies are missing, its default dependencies are provided
    """
  static let customDependencies = "When a dependent has configured dependencies, those dependencies are provided"

  static let defaultSetDependenciesImplementation = """
    A dependent has a default `setDependencies(_:)` implementation that sets its configured dependencies
    """
  static let customSetDependenciesImplementation = """
    A dependent can override the default `setDependencies(_:)` implementation
    """

  static let defaultResetDependenciesImplementation = """
    A dependent has a default `resetDependencies()` implementation that clears its configured dependencies
    """
  static let customResetDependenciesImplementation = """
    A dependent can override the default `resetDependencies()` implementation
    """

  static let missingDependencyDynamicMemberLookup = """
    When a dependent's dependencies are missing, dynamic lookup of a dependency as a property yields a nil value
    """
  static let dynamicMemberLookup = "The discrete dependencies of a dependent can be accessed dynamically as properties"
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import FBSDKGamingServicesKit
import XCTest

final class ShareTournamentDialogURLBuilderTests: XCTestCase {
  let expirationDate = DateFormatter.format(ISODateString: "2021-09-24T18:03:47+0000")
  lazy var updateTournament = Tournament(
    identifier: "1234",
    payload: "Hello"
  )
  lazy var tournamentConfiguration = TournamentConfig(
    title: "Test",
    endTime: expirationDate,
    scoreType: .numeric,
    sortOrder: .higherIsBetter,
    payload: "Hello"
  )

  func testUpdateURL() throws {
    let expectURLComponents = try XCTUnwrap(
      URLComponents(
        string: "https://fb.gg/me/instant_tournament/12345?tournament_id=1234&payload=Hello&score=1000"
      )
    )
    let updateURL = try XCTUnwrap(
      ShareTournamentDialogURLBuilder
        .update(updateTournament)
        .url(withPathAppID: "12345", score: 1000)
    )
    let updateURLComponents = try XCTUnwrap(
      URLComponents(
        url: updateURL,
        resolvingAgainstBaseURL: false
      )
    )
    let updateURLQueryItems: [URLQueryItem] = try XCTUnwrap(updateURLComponents.queryItems)
    let expectedQueryItems: [URLQueryItem] = try XCTUnwrap(expectURLComponents.queryItems)

    XCTAssertEqual(updateURLComponents.scheme, expectURLComponents.scheme)
    XCTAssertEqual(updateURLComponents.host, expectURLComponents.host)
    XCTAssertEqual(updateURLComponents.path, expectURLComponents.path)
    XCTAssertEqual(updateURLQueryItems.count, expectedQueryItems.count, "Should contain the same number of query items")
  }

  func testCreateURL() throws {
    let expectURLComponents = try XCTUnwrap(
      URLComponents(
        string: "https://fb.gg/me/instant_tournament/12345?score=1000&end_time=1632506627&tournament_title=Test&score_format=NUMERIC&sort_order=HIGHER_IS_BETTER&tournament_payload=Hello"
      )
    )

    let updateURL = try XCTUnwrap(
      ShareTournamentDialogURLBuilder
        .create(tournamentConfiguration)
        .url(withPathAppID: "12345", score: 1000)
    )
    let updateURLComponents = try XCTUnwrap(
      URLComponents(
        url: updateURL,
        resolvingAgainstBaseURL: false
      )
    )
    let updateURLQueryItems: [URLQueryItem] = try XCTUnwrap(updateURLComponents.queryItems)
    let expectedQueryItems: [URLQueryItem] = try XCTUnwrap(expectURLComponents.queryItems)

    XCTAssertEqual(updateURLComponents.scheme, expectURLComponents.scheme)
    XCTAssertEqual(updateURLComponents.host, expectURLComponents.host)
    XCTAssertEqual(updateURLComponents.path, expectURLComponents.path)
    XCTAssertEqual(updateURLQueryItems.count, expectedQueryItems.count, "Should contain the same number of query items")
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import CommonCrypto
import FBSDKCoreKit
import Foundation

/**
 Represents a code verifier used in the PKCE (Proof Key for Code Exchange)
 process. This is a cryptographically random string using the characters
 A-Z, a-z, 0-9, and the punctuation characters -._~ (hyphen, period,
 underscore, and tilde), between 43 and 128 characters long.
 */
@objcMembers
@objc(FBSDKCodeVerifier)
public final class CodeVerifier: NSObject {

  /// The string value of the code verifier
  public let value: String

  /// The SHA256 hashed challenge of the code verifier
  public var challenge: String {
    guard let data = value.data(using: .utf8) else {
      return ""
    }
    var sha256Data = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
    data.withUnsafeBytes {
      _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &sha256Data)
    }
    return Self.base64URLEncodeDataWithNoPadding(data: Data(sha256Data))
  }

  private static let codeVerifierByteCount = 72
  private static let codeVerifierStringLength = 43 ... 128

  /**
   Attempts to initialize a new code verifier instance with the given string.
   Creation will fail and return nil if the string is invalid.

   @param string the code verifier string
   */
  @objc(initWithString:)
  public convenience init?(string: String) {
    guard Self.codeVerifierStringLength.contains(string.count) else { return nil }

    let invalidCharacters = CharacterSet(charactersIn: "-._~")
      .union(.alphanumerics)
      .inverted

    guard string.rangeOfCharacter(from: invalidCharacters) == nil else { return nil }

    self.init(validString: string)
  }

  /**
   Initializes a new code verifier instance with a random string value
   */
  public override convenience init() {
    var randomData = [Int8](repeating: 0, count: Self.codeVerifierByteCount)

    guard SecRandomCopyBytes(kSecRandomDefault, Self.codeVerifierByteCount, &randomData) == 0 else {
      fatalError("Unable to create random data for code verifier value")
    }

    let codeVerifier = Self.base64URLEncodeDataWithNoPadding(data: Data(bytes: randomData, count: randomData.count))

    self.init(validString: codeVerifier)
  }

  private init(validString string: String) {
    value = string
    super.init()
  }

  private static func base64URLEncodeDataWithNoPadding(data: Data) -> String {
    data
      .base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

/**
 Passed to openURL to indicate which default audience to use for sessions that post data to Facebook.

 Certain operations such as publishing a status or publishing a photo require an audience. When the user
 grants an application permission to perform a publish operation, a default audience is selected as the
 publication ceiling for the application. This enumerated value allows the application to select which
 audience to ask the user to grant publish permission for.
 */
@objc(FBSDKDefaultAudience)
public enum DefaultAudience: UInt {
  /// Indicates that the user's friends are able to see posts made by the application
  case friends
  /// Indicates that only the user is able to see posts made by the application
  case onlyMe
  /// Indicates that all Facebook users are able to see posts made by the application
  case everyone
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

/**
 Describes the initial response when starting the device login flow.
 This is used by `DeviceLoginManager`.
 */
@objcMembers
@objc(FBSDKDeviceLoginCodeInfo)
public final class DeviceLoginCodeInfo: NSObject {
  /// The unique id for this login flow.
  public let identifier: String

  /// The short "user_code" that should be presented to the user.
  public let loginCode: String

  /// The verification URL.
  public let verificationURL: URL

  /// The expiration date.
  public let expirationDate: Date

  /// The polling interval
  public let pollingInterval: UInt

  private static let minPollingInterval: UInt = 5

  public init(
    identifier: String,
    loginCode: String,
    verificationURL: URL,
    expirationDate: Date,
    pollingInterval: UInt
  ) {
    self.identifier = identifier
    self.loginCode = loginCode
    self.verificationURL = verificationURL
    self.expirationDate = expirationDate
    self.pollingInterval = max(pollingInterval, Self.minPollingInterval)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

/// Custom error type for device login errors in the login error domain
public struct DeviceLoginError: CustomNSError, Hashable {

  let _nsError: NSError? // swiftlint:disable:this identifier_name

  public let errorCode: Int

  public let errorUserInfo: [String: Any]

  public init(_nsError nsError: NSError) {
    _nsError = nsError
    errorCode = nsError.code
    errorUserInfo = nsError.userInfo
  }

  public init(_ code: Code, userInfo: [String: Any] = [:]) {
    _nsError = nil
    errorCode = code.rawValue
    errorUserInfo = userInfo
  }

  public static var errorDomain: String { LoginErrorDomain }

  public typealias Code = DeviceLoginErrorCode

  /// Your device is polling too frequently.
  public static var excessivePolling: Code { .excessivePolling }

  /// User has declined to authorize your application.
  public static var authorizationDeclined: Code { .authorizationDeclined }

  /// User has not yet authorized your application. Continue polling.
  public static var authorizationPending: Code { .authorizationPending }

  /// The code you entered has expired.
  public static var codeExpired: Code { .codeExpired }

  public static func == (lhs: Self, rhs: Self) -> Bool {
    if let leftError = lhs._nsError,
       let rightError = rhs._nsError {
      return leftError === rightError
    } else {
      return lhs.errorCode == rhs.errorCode
    }
  }

  public func hash(into hasher: inout Hasher) {
    hasher.combine(errorCode)
  }
}

/// Custom error codes for device login errors in the login error domain
@objc(FBSDKDeviceLoginError) public enum DeviceLoginErrorCode: Int, @unchecked Sendable, Equatable {

  public typealias _ErrorType = DeviceLoginError

  /// Your device is polling too frequently.
  case excessivePolling = 1349172

  /// User has declined to authorize your application.
  case authorizationDeclined = 1349173

  /// User has not yet authorized your application. Continue polling.
  case authorizationPending = 1349174

  /// The code you entered has expired.
  case codeExpired = 1349152
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit
import FBSDKCoreKit_Basics
import Foundation

/**
 Use this class to perform a device login flow.
 The device login flow starts by requesting a code from the device login API.
   This class informs the delegate when this code is received. You should then present the
   code to the user to enter. In the meantime, this class polls the device login API
   periodically and informs the delegate of the results.

 See [Facebook Device Login](https://developers.facebook.com/docs/facebook-login/for-devices).
 */
@objcMembers
@objc(FBSDKDeviceLoginManager)
public final class DeviceLoginManager: NSObject {

  private static var loginManagerInstances = [DeviceLoginManager]()

  /// The device login manager delegate.
  public weak var delegate: DeviceLoginManagerDelegate?

  /// The requested permissions.
  public let permissions: [String]

  /**
   The optional URL to redirect the user to after they complete the login.
   The URL must be configured in your App Settings -> Advanced -> OAuth Redirect URIs
   */
  public var redirectURL: URL?

  var codeInfo: DeviceLoginCodeInfo?
  private var isCancelled = false
  private let isSmartLoginEnabled: Bool

  var configuredDependencies: ObjectDependencies?

  var defaultDependencies: ObjectDependencies? = .init(
    devicePoller: DevicePoller(),
    errorFactory: _ErrorFactory(),
    graphRequestFactory: GraphRequestFactory(),
    internalUtility: InternalUtility.shared,
    settings: Settings.shared
  )

  /**
   Initializes a new instance.
   @param permissions The permissions to request.
   @param enableSmartLogin Whether to enable smart login.
   */

  @objc(initWithPermissions:enableSmartLogin:)
  public init(permissions: [String], enableSmartLogin: Bool) {
    self.permissions = permissions
    isSmartLoginEnabled = enableSmartLogin
    super.init()
  }

  /**
   Starts the device login flow
   This instance will retain self until the flow is finished or cancelled.
   */
  public func start() {
    guard let dependencies = try? getDependencies() else { return }

    dependencies.internalUtility.validateAppID()
    Self.loginManagerInstances.append(self)

    let parameters: [String: Any] = [
      "scope": permissions.joined(separator: ","),
      "redirect_uri": redirectURL?.absoluteString ?? "",
      "device_info": DeviceRequestsHelper.getDeviceInfo(),
    ]
    let request = dependencies.graphRequestFactory.createGraphRequest(
      withGraphPath: "device/login",
      parameters: parameters,
      tokenString: dependencies.internalUtility.validateRequiredClientAccessToken(),
      httpMethod: .post,
      flags: []
    )
    request.isGraphErrorRecoveryDisabled = true

    request.start { [self] _, anyResult, potentialError in
      if let error = potentialError {
        return processError(error)
      }

      let result = anyResult as? [String: Any]
      let verificationURL = (result?["verification_uri"] as? String)
        .flatMap(URL.init(string:))

      guard
        let url = verificationURL,
        let identifier = result?["code"] as? String,
        let loginCode = result?["user_code"] as? String
      else {
        return notifyDelegate(
          error: dependencies.errorFactory.error(
            code: CoreError.errorUnknown.rawValue,
            message: "Unable to create a login request",
            underlyingError: nil
          )
        )
      }

      let expirationInterval = (result?["expires_in"] as? String)
        .flatMap(TimeInterval.init) ?? 0.0
      let pollingInterval = (result?["interval"] as? NSNumber)?.uintValue ?? 0

      let codeInfo = DeviceLoginCodeInfo(
        identifier: identifier,
        loginCode: loginCode,
        verificationURL: url,
        expirationDate: Date().addingTimeInterval(expirationInterval),
        pollingInterval: pollingInterval
      )
      self.codeInfo = codeInfo

      if isSmartLoginEnabled {
        DeviceRequestsHelper.startAdvertisementService(loginCode: codeInfo.loginCode, delegate: self)
      }

      delegate?.deviceLoginManager(self, startedWith: codeInfo)
      schedulePoll(interval: codeInfo.pollingInterval)
    }
  }

  /// Attempts to cancel the device login flow.
  public func cancel() {
    DeviceRequestsHelper.cleanUpAdvertisementService(for: self)
    isCancelled = true
    Self.loginManagerInstances.removeAll { $0 === self }
  }

  private func notifyDelegate(error: Error) {
    DeviceRequestsHelper.cleanUpAdvertisementService(for: self)
    delegate?.deviceLoginManager(self, completedWith: nil, error: error)
    Self.loginManagerInstances.removeAll { $0 === self }
  }

  func notifyDelegate(
    token tokenString: String?,
    expirationDate: Date?,
    dataAccessExpirationDate: Date?
  ) {
    DeviceRequestsHelper.cleanUpAdvertisementService(for: self)

    let complete: (DeviceLoginManagerResult) -> Void = { [self] result in
      delegate?.deviceLoginManager(self, completedWith: result, error: nil)
      Self.loginManagerInstances.removeAll { $0 === self }
    }

    guard let tokenString = tokenString else {
      isCancelled = true
      let result = DeviceLoginManagerResult(token: nil, isCancelled: true)
      return complete(result)
    }

    guard let dependencies = try? getDependencies() else { return }

    let request = dependencies.graphRequestFactory.createGraphRequest(
      withGraphPath: "me",
      parameters: ["fields": "id,permissions"],
      tokenString: tokenString,
      httpMethod: .get,
      flags: [.disableErrorRecovery]
    )

    request.start { [self] _, anyResult, potentialError in
      guard
        potentialError == nil,
        let graphResult = anyResult as? [String: Any],
        let userID = graphResult["id"] as? String,
        let permissionResult = graphResult["permissions"] as? [String: Any]
      else {
        return notifyDelegate(
          error: dependencies.errorFactory.error(
            domain: LoginErrorDomain,
            code: CoreError.errorUnknown.rawValue,
            userInfo: nil,
            message: "Unable to fetch permissions for token",
            underlyingError: potentialError
          )
        )
      }

      let permissions = NSMutableSet()
      let declinedPermissions = NSMutableSet()
      let expiredPermissions = NSMutableSet()

      dependencies.internalUtility.extractPermissions(
        fromResponse: permissionResult,
        grantedPermissions: permissions,
        declinedPermissions: declinedPermissions,
        expiredPermissions: expiredPermissions
      )
      let accessToken = AccessToken(
        tokenString: tokenString,
        permissions: permissions.compactMap { $0 as? String },
        declinedPermissions: declinedPermissions.compactMap { $0 as? String },
        expiredPermissions: expiredPermissions.compactMap { $0 as? String },
        appID: dependencies.settings.appID ?? "",
        userID: userID,
        expirationDate: expirationDate,
        refreshDate: nil,
        dataAccessExpirationDate: dataAccessExpirationDate
      )
      let result = DeviceLoginManagerResult(token: accessToken, isCancelled: false)
      AccessToken.current = accessToken
      complete(result)
    }
  }

  func processError(_ error: Error) {
    let nsError = error as NSError
    let code = (nsError.userInfo[GraphRequestErrorGraphErrorSubcodeKey] as? Int)
      .flatMap(DeviceLoginError.Code.init(rawValue:))

    switch (code, codeInfo) {
    case let (.authorizationPending, info?):
      schedulePoll(interval: info.pollingInterval)
    case (.codeExpired, _), (.authorizationDeclined, _):
      notifyDelegate(token: nil, expirationDate: nil, dataAccessExpirationDate: nil)
    case let (.excessivePolling, info?):
      schedulePoll(interval: info.pollingInterval * 2)
    default:
      notifyDelegate(error: error)
    }
  }

  func schedulePoll(interval: UInt) {
    guard let dependencies = try? getDependencies() else { return }

    dependencies.devicePoller.schedule(interval: interval) { [self] in
      guard !isCancelled else { return }

      let parameters: [String: Any] = ["code": codeInfo?.identifier ?? NSNull()]
      let request = dependencies.graphRequestFactory.createGraphRequest(
        withGraphPath: "device/login_status",
        parameters: parameters,
        tokenString: dependencies.internalUtility.validateRequiredClientAccessToken(),
        httpMethod: .post,
        flags: []
      )
      request.isGraphErrorRecoveryDisabled = true

      request.start { [self] _, anyResult, potentialError in
        guard !isCancelled else { return }

        if let error = potentialError {
          processError(error)
        } else {
          let result = anyResult as? [String: Any]
          let tokenString = result?["access_token"] as? String
          var expirationDate = Date.distantFuture
          let expirationInterval = (result?["expires_in"] as? String).flatMap(Int.init) ?? 0
          if expirationInterval > 0 {
            expirationDate = Date(timeIntervalSinceNow: TimeInterval(expirationInterval))
          }

          var dataAccessExpirationDate = NSDate.distantFuture
          let dataAccessExpirationTime = (result?["data_access_expiration_time"] as? String).flatMap(Int.init) ?? 0
          if dataAccessExpirationTime > 0 {
            dataAccessExpirationDate = Date(timeIntervalSince1970: TimeInterval(dataAccessExpirationTime))
          }

          if let tokenString = tokenString {
            notifyDelegate(
              token: tokenString,
              expirationDate: expirationDate,
              dataAccessExpirationDate: dataAccessExpirationDate
            )
          } else {
            notifyDelegate(
              error: dependencies.errorFactory.error(
                domain: LoginErrorDomain,
                code: CoreError.errorUnknown.rawValue,
                userInfo: nil,
                message: "Device Login poll failed. No token or error was found.",
                underlyingError: nil
              )
            )
          }
        }
      }
    }
  }
}

extension DeviceLoginManager: NetServiceDelegate {
  public func netService(_ service: NetService, didNotPublish errorValues: [String: NSNumber]) {
    // Only cleanup if the publish error is from our advertising service
    guard DeviceRequestsHelper.isDelegate(self, forAdvertisementService: service) else { return }

    DeviceRequestsHelper.cleanUpAdvertisementService(for: self)
  }
}

extension DeviceLoginManager: DependentAsObject {
  struct ObjectDependencies {
    var devicePoller: DevicePolling
    var errorFactory: ErrorCreating
    var graphRequestFactory: GraphRequestFactoryProtocol
    var internalUtility: InternalUtilityProtocol
    var settings: SettingsProtocol
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

/// A delegate for `DeviceLoginManager`.
@objc(FBSDKDeviceLoginManagerDelegate)
public protocol DeviceLoginManagerDelegate {

  /**
   Indicates the device login flow has started. You should parse `codeInfo` to present the code to the user to enter.
   @param loginManager the login manager instance.
   @param codeInfo the code info data.
   */
  @objc(deviceLoginManager:startedWithCodeInfo:)
  func deviceLoginManager(
    _ loginManager: DeviceLoginManager,
    startedWith codeInfo: DeviceLoginCodeInfo
  )

  /**
   Indicates the device login flow has finished.
   @param loginManager the login manager instance.
   @param result the results of the login flow.
   @param error the error, if available.
   The flow can be finished if the user completed the flow, cancelled, or if the code has expired.
   */
  @objc(deviceLoginManager:completedWithResult:error:)
  func deviceLoginManager(
    _ loginManager: DeviceLoginManager,
    completedWith result: DeviceLoginManagerResult?,
    error: Error?
  )
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit
import Foundation

/// Represents the results of the a device login flow. This is used by `DeviceLoginManager`
@objcMembers
@objc(FBSDKDeviceLoginManagerResult)
public final class DeviceLoginManagerResult: NSObject {

  /// The token
  public private(set) var accessToken: AccessToken?

  /// Indicates if the login was cancelled by the user, or if the device login code has expired.
  public private(set) var isCancelled: Bool

  /**
   Internal method exposed to facilitate transition to Swift.
   API Subject to change or removal without warning. Do not use.

   @warning INTERNAL - DO NOT USE
   */
  public init(
    token: AccessToken?,
    isCancelled cancelled: Bool
  ) {
    accessToken = token
    isCancelled = cancelled
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

protocol DevicePolling {
  func schedule(interval: UInt, block: @escaping () -> Void)
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit
import UIKit

/**
 A button that initiates a log in or log out flow upon tapping.

 `LoginButton` works with `AccessToken.current` to determine what to display,
 and automatically starts authentication when tapped (i.e., you do not need to manually subscribe action targets).

 Like `LoginManager`, you should make sure your app delegate is connected to `ApplicationDelegate`
 in order for the button's delegate to receive messages.

 `LoginButton` has a fixed height of 30 pixels, but you may change the width.
 Initializing the button with `nil` frame will size the button to its minimum frame.
 */
@objcMembers
@objc(FBSDKLoginButton)
public final class FBLoginButton: FBButton {

  /// The default audience to use, if publish permissions are requested at login time.
  public var defaultAudience: DefaultAudience {
    get { loginProvider.defaultAudience }
    set { loginProvider.defaultAudience = newValue }
  }

  /// Gets or sets the delegate.
  @IBOutlet public weak var delegate: LoginButtonDelegate? // swiftlint:disable:this private_outlet

  /**
   The permissions to request.
   To provide the best experience, you should minimize the number of permissions you request, and only ask for them when needed.
   For example, do not ask for "user_location" until you the information is actually used by the app.

   Note this is converted to NSSet and is only
   an NSArray for the convenience of literal syntax.

   See [the permissions guide]( https://developers.facebook.com/docs/facebook-login/permissions/ ) for more details.
   */
  public var permissions: [String] = []

  /// Gets or sets the desired tooltip behavior.
  public var tooltipBehavior: FBLoginButton.TooltipBehavior = .automatic

  /// Gets or sets the desired tooltip color style.
  public var tooltipColorStyle: FBTooltipView.ColorStyle = .friendlyBlue

  /// Gets or sets the desired tracking preference to use for login attempts. Defaults to `.enabled`
  public var loginTracking: LoginTracking = .enabled

  /**
   Gets or sets an optional nonce to use for login attempts. A valid nonce must be a non-empty string without whitespace.
   An invalid nonce will not be set. Instead, default unique nonces will be used for login attempts.
   */
  public var nonce: String? {
    get {
      nonceValue
    }

    set {
      if let nonce = newValue,
         NonceValidator.isValid(nonce: nonce) {
        nonceValue = nonce
      } else {
        nonceValue = nil
        let msg = "Unable to set invalid nonce: \(String(describing: nonce)) on FBLoginButton"
        _Logger.singleShotLogEntry(.developerErrors, logEntry: msg)
      }
    }
  }

  /// Gets or sets an optional page id to use for login attempts.
  public var messengerPageId: String?

  /// Gets or sets the login authorization type to use in the login request. Defaults to `rerequest`. Use `nil` to avoid
  /// requesting permissions that were previously denied.
  public var authType: LoginAuthType? = .rerequest

  /// The code verifier used in the PKCE process.
  /// If not provided, a code verifier will be randomly generated.
  public var codeVerifier = CodeVerifier()

  private var nonceValue: String?
  private var hasShownTooltipBubble = false

  var userID: String?
  var userName: String?
  var elementProvider: UserInterfaceElementProviding = InternalUtility.shared
  var stringProvider: UserInterfaceStringProviding = InternalUtility.shared
  var loginProvider: LoginProviding = LoginManager()
  var graphRequestFactory: GraphRequestFactoryProtocol = GraphRequestFactory()

  var isAuthenticated: Bool {
    AccessToken.current != nil || AuthenticationToken.current != nil
  }

  /**
    Indicates the desired login tooltip behavior.
   */
  @objc(FBSDKLoginButtonTooltipBehavior)
  public enum TooltipBehavior: UInt {
    /** The default behavior. The tooltip will only be displayed if
     the app is eligible (determined by possible server round trip) */
    case automatic = 0

    /// Force display of the tooltip (typically for UI testing)
    case forceDisplay

    /** Force disable. In this case you can still exert more refined
     control by manually constructing a `FBSDKLoginTooltipView` instance. */
    case disable
  }

  private enum ViewGeometry {
    static let logoSize = 16.0
    static let logoLeftMargin = 6.0
    static let buttonHeight = 28.0
    static let rightMargin = 8.0
    static let paddingBetweenLogoTitle = 8.0
  }

  // MARK: - Initialization

  public override init(frame: CGRect) {
    super.init(frame: frame)
    configureButton()
  }

  required init?(coder: NSCoder) {
    super.init(coder: coder)
    configureButton()
  }

  convenience init(
    frame: CGRect = .zero,
    elementProvider: UserInterfaceElementProviding,
    stringProvider: UserInterfaceStringProviding,
    loginProvider: LoginProviding,
    graphRequestFactory: GraphRequestFactoryProtocol
  ) {
    self.init(frame: frame)
    self.elementProvider = elementProvider
    self.stringProvider = stringProvider
    self.loginProvider = loginProvider
    self.graphRequestFactory = graphRequestFactory
  }

  /**
   Create a new `LoginButton` with a given optional frame and read permissions.

   - Parameter frame: Optional frame to initialize with. Default: `nil`, which uses a default size for the button.
   - Parameter permissions: Array of read permissions to request when logging in.
   */
  convenience init(frame: CGRect = .zero, permissions: [Permission] = [.publicProfile]) {
    self.init(frame: frame)
    self.permissions = permissions.map { $0.name }
  }

  private func configureButton() {
    let logInTitle = shortLogInTitle()
    let logOutTitle = logOutTitle()

    configure(
      with: nil,
      title: logInTitle,
      backgroundColor: backgroundColor,
      highlightedColor: nil,
      selectedTitle: logOutTitle,
      selectedIcon: nil,
      selectedColor: backgroundColor,
      selectedHighlightedColor: nil
    )

    titleLabel?.textAlignment = .center
    let heightConstraint = heightAnchor.constraint(equalToConstant: ViewGeometry.buttonHeight)
    heightConstraint.isActive = true
    addConstraint(heightConstraint)

    initializeContent()
    addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
    addNotificationObservers()
  }

  // MARK: - UIView

  public override func didMoveToWindow() {
    super.didMoveToWindow()

    if window != nil,
       tooltipBehavior == .forceDisplay || !hasShownTooltipBubble {
      showTooltipIfNeeded()
      hasShownTooltipBubble = true
    }
  }

  // MARK: - Layout

  public override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
    CGRect(
      x: ViewGeometry.logoLeftMargin,
      y: contentRect.midY - ViewGeometry.logoSize / 2,
      width: ViewGeometry.logoSize,
      height: ViewGeometry.logoSize
    )
  }

  public override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
    guard
      !isHidden,
      !bounds.isEmpty
    else { return .zero }

    let imageRect = imageRect(forContentRect: contentRect)
    let titleX = imageRect.maxX + ViewGeometry.paddingBetweenLogoTitle

    return CGRect(
      x: titleX,
      y: 0,
      width: contentRect.width - titleX - ViewGeometry.rightMargin,
      height: contentRect.height
    )
  }

  public override func layoutSubviews() {
    let size = bounds.size
    let longTitleSize = sizeThatFits(size, title: longLogInTitle())
    let title = longTitleSize.width <= size.width ? longLogInTitle() : shortLogInTitle()
    if title != self.title(for: .normal) {
      setTitle(title, for: .normal)
    }
    super.layoutSubviews()
  }

  public override func sizeThatFits(_ size: CGSize) -> CGSize {
    guard
      !isHidden,
      let titleLabel = titleLabel,
      let font = titleLabel.font
    else { return .zero }

    let selectedSize = textSize(
      forText: logOutTitle(),
      font: font,
      constrainedSize: size,
      lineBreakMode: titleLabel.lineBreakMode
    )

    var normalSize = textSize(
      forText: longLogInTitle(),
      font: font,
      constrainedSize: size,
      lineBreakMode: titleLabel.lineBreakMode
    )

    if normalSize.width > size.width {
      normalSize = textSize(
        forText: shortLogInTitle(),
        font: font,
        constrainedSize: size,
        lineBreakMode: titleLabel.lineBreakMode
      )
    }

    let titleWidth = max(normalSize.width, selectedSize.width)
    let buttonWidth = ViewGeometry.logoLeftMargin
      + ViewGeometry.logoSize
      + ViewGeometry.paddingBetweenLogoTitle
      + titleWidth
      + ViewGeometry.rightMargin

    return CGSize(width: buttonWidth, height: ViewGeometry.buttonHeight)
  }

  // MARK: - Notifications

  private func addNotificationObservers() {
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(profileDidChange),
      name: .ProfileDidChange,
      object: nil
    )
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(accessTokenDidChange),
      name: .AccessTokenDidChange,
      object: nil
    )
  }

  func accessTokenDidChange(_ notification: Notification) {
    guard let userInfo = notification.userInfo else { return }

    if userInfo[AccessTokenDidChangeUserIDKey] != nil || userInfo[AccessTokenDidExpireKey] != nil {
      updateContentForAccessToken()
    }
  }

  func profileDidChange(_ notification: Notification) {
    updateContentForUser(.current)
  }

  // MARK: - Button Configuration

  func buttonPressed(_ sender: Any) {
    if isAuthenticated {
      if loginTracking != .limited {
        logTapEvent(withEventName: .loginButtonDidTap, parameters: nil)
      }

      presentAlertViewController()
    } else {
      if let loginButtonWillLogin = delegate?.loginButtonWillLogin, !loginButtonWillLogin(self) {
        return
      }

      logInUser()
    }
  }

  func makeLoginConfiguration() -> LoginConfiguration? {
    let nonce = nonce ?? UUID().uuidString
    return LoginConfiguration(
      permissions: Set(permissions.map(Permission.init(stringLiteral:))),
      tracking: loginTracking,
      nonce: nonce,
      messengerPageId: messengerPageId,
      authType: authType,
      codeVerifier: codeVerifier
    )
  }

  private func logInUser() {

    let loginConfiguration = makeLoginConfiguration()

    if loginTracking == .enabled {
      logTapEvent(withEventName: .loginButtonDidTap, parameters: nil)
    }

    if let loginConfiguration = loginConfiguration {
      loginProvider.logIn(
        viewController: elementProvider.viewController(for: self),
        configuration: loginConfiguration
      ) { result in
        self.delegate?.loginButton(self, didCompleteWith: result.loginManagerResult, error: result.error)
      }
    }
  }

  private func presentAlertViewController() {
    let title: String

    if let userName = userName {
      let localizedFormatString = NSLocalizedString(
        "LoginButton.LoggedInAs",
        tableName: "FacebookSDK",
        bundle: stringProvider.bundleForStrings,
        value: "Logged in as %@",
        comment: "The format string for the FBLoginButton label when the user is logged in"
      )
      title = String.localizedStringWithFormat(localizedFormatString, userName)
    } else {
      let localizedLoggedIn = NSLocalizedString(
        "LoginButton.LoggedIn",
        tableName: "FacebookSDK",
        bundle: stringProvider.bundleForStrings,
        value: "Logged in using Facebook",
        comment: "The fallback string for the FBLoginButton label when the user name is not available yet"
      )
      title = localizedLoggedIn
    }

    let cancelTitle = NSLocalizedString(
      "LoginButton.CancelLogout",
      tableName: "FacebookSDK",
      bundle: stringProvider.bundleForStrings,
      value: "Cancel",
      comment: "The label for the FBLoginButton action sheet to cancel logging out"
    )

    let logOutTitle = NSLocalizedString(
      "LoginButton.ConfirmLogOut",
      tableName: "FacebookSDK",
      bundle: stringProvider.bundleForStrings,
      value: "Log Out",
      comment: "The label for the FBLoginButton action sheet to confirm logging out"
    )

    let alertController = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
    alertController.popoverPresentationController?.sourceView = self
    alertController.popoverPresentationController?.sourceRect = bounds

    let cancel = UIAlertAction(title: cancelTitle, style: .cancel, handler: nil)
    let logout = UIAlertAction(title: logOutTitle, style: .destructive) { _ in
      self.logout()
    }

    alertController.addAction(cancel)
    alertController.addAction(logout)

    let topMostViewController = elementProvider.topMostViewController()
    topMostViewController?.present(alertController, animated: true)
  }

  private func logOutTitle() -> String {
    NSLocalizedString(
      "LoginButton.LogOut",
      tableName: "FacebookSDK",
      bundle: stringProvider.bundleForStrings,
      value: "Log out",
      comment: "The label for the FBLoginButton when the user is currently logged in"
    )
  }

  private func longLogInTitle() -> String {
    NSLocalizedString(
      "LoginButton.LogInContinue",
      tableName: "FacebookSDK",
      bundle: stringProvider.bundleForStrings,
      value: "Continue with Facebook",
      comment: "The long label for the FBLoginButton when the user is currently logged out"
    )
  }

  private func shortLogInTitle() -> String {
    NSLocalizedString(
      "LoginButton.LogIn",
      tableName: "FacebookSDK",
      bundle: stringProvider.bundleForStrings,
      value: "Log in",
      comment: "The short label for the FBLoginButton when the user is currently logged out"
    )
  }

  private func showTooltipIfNeeded() {
    guard
      !isAuthenticated,
      tooltipBehavior != .disable
    else { return }

    let tooltipView = FBLoginTooltipView()
    tooltipView.colorStyle = tooltipColorStyle

    if tooltipBehavior == .forceDisplay {
      tooltipView.shouldForceDisplay = true
    }

    tooltipView.present(from: self)
  }

  // MARK: - Content

  // On initial setting of button state. We want to update the button's user
  // information using the most comprehensive available.
  // If access token is available use that.
  // If only profile is available, use that.
  func initializeContent() {
    if AccessToken.current != nil {
      updateContentForAccessToken()
    } else if let profile = Profile.current {
      updateContentForUser(profile)
    } else {
      isSelected = false
    }
  }

  func updateContentForAccessToken() {
    let accessTokenIsValid = AccessToken.isCurrentAccessTokenActive
    isSelected = accessTokenIsValid

    if accessTokenIsValid,
       AccessToken.current?.userID != userID {
      fetchAndSetContent()
    }
  }

  func fetchAndSetContent() {
    let request = graphRequestFactory.createGraphRequest(
      withGraphPath: "me",
      parameters: ["fields": "id,name"],
      flags: .disableErrorRecovery
    )

    request.start { _, result, error in
      guard
        let dict = result as? [String: String],
        let userID = dict["id"],
        error == nil,
        let currentUserID = AccessToken.current?.userID,
        currentUserID == userID
      else { return }

      self.userName = dict["name"] ?? ""
      self.userID = userID
    }
  }

  func updateContentForUser(_ profile: Profile?) {
    guard let profile = profile else {
      isSelected = false
      return
    }

    isSelected = true

    if userInformationDoesNotMatch(profile) {
      userName = profile.name ?? ""
      userID = profile.userID
    }
  }

  private func userInformationDoesNotMatch(_ profile: Profile) -> Bool {
    profile.userID != userID || profile.name != userName
  }

  func logout() {
    loginProvider.logOut()
    delegate?.loginButtonDidLogOut(self)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit
import UIKit

/**
 Represents a tooltip to be displayed next to a Facebook login button
 to highlight features for new users.

 The `FBSDKLoginButton` may display this view automatically. If you do
 not use the `FBSDKLoginButton`, you can manually call one of the `present*` methods
 as appropriate and customize behavior via `FBSDKLoginTooltipViewDelegate` delegate.

 By default, the `FBSDKLoginTooltipView` is not added to the superview until it is
 determined the app has migrated to the new login experience. You can override this
 (e.g., to test the UI layout) by implementing the delegate or setting `forceDisplay` to YES.
 */
@objc(FBSDKLoginTooltipView)
public final class FBLoginTooltipView: FBTooltipView {
  /// the delegate
  @objc public weak var delegate: LoginTooltipViewDelegate?

  /**
   if set to YES, the view will always be displayed and the delegate's
   `loginTooltipView:shouldAppear:` will NOT be called.
   */
  @objc public var forceDisplay = false

  /**
   if set to YES, the view will always be displayed and the delegate's
   `loginTooltipView:shouldAppear:` will NOT be called.
   */
  @objc public var shouldForceDisplay: Bool {
    get {
      forceDisplay
    }
    set {
      forceDisplay = newValue
    }
  }

  /// Service configuration provider
  let serverConfigurationProvider: ServerConfigurationProviding

  /// UI String provider
  let stringProvider: UserInterfaceStringProviding

  // MARK: - Init

  /// Create tooltip
  @objc public convenience init() {
    self.init(
      serverConfigurationProvider: ServerConfigurationProvider(),
      stringProvider: InternalUtility.shared
    )
  }

  @objc(initWithTagline:message:colorStyle:)
  public override init(tagline: String?, message: String?, colorStyle: FBTooltipView.ColorStyle) {
    serverConfigurationProvider = ServerConfigurationProvider()
    stringProvider = InternalUtility.shared
    super.init(tagline: tagline, message: message, colorStyle: colorStyle)
  }

  /// Create tooltip
  /// - Parameters:
  ///   - serverConfigurationProvider: Service configuration provider
  ///   - stringProvider: String provider
  init(
    serverConfigurationProvider: ServerConfigurationProviding,
    stringProvider: UserInterfaceStringProviding
  ) {
    self.serverConfigurationProvider = serverConfigurationProvider
    self.stringProvider = stringProvider

    let tooltipMessage = NSLocalizedString(
      "LoginTooltip.Message",
      tableName: "FacebookSDK",
      bundle: stringProvider.bundleForStrings,
      value: "You're in control - choose what info you want to share with apps.",
      comment: "The message of the FBSDKLoginTooltipView"
    )

    super.init(tagline: nil, message: tooltipMessage, colorStyle: .friendlyBlue)
  }

  public override func present(in view: UIView, arrowPosition: CGPoint, direction: FBTooltipView.ArrowDirection) {
    if shouldForceDisplay {
      super.present(in: view, arrowPosition: arrowPosition, direction: direction)
    } else {
      fetchTooltipConfiguration(with: view, arrowPosition: arrowPosition, direction: direction)
    }
  }

  /// Fetch tooltip configuration
  /// - Parameters:
  ///   - view: Target view
  ///   - arrowPosition: Arrow position
  ///   - direction: Arrow direction of tooltip
  private func fetchTooltipConfiguration(
    with view: UIView,
    arrowPosition: CGPoint,
    direction: FBTooltipView.ArrowDirection
  ) {
    serverConfigurationProvider.loadServerConfiguration { loginTooltip, _ in
      guard let loginTooltip = loginTooltip else {
        self.delegate?.loginTooltipViewWillNotAppear?(self)
        return
      }
      self.message = loginTooltip.text

      var shouldDisplay = loginTooltip.isEnabled
      shouldDisplay = self.delegate?.loginTooltipView?(self, shouldAppear: shouldDisplay) ?? shouldDisplay

      if shouldDisplay {
        super.present(in: view, arrowPosition: arrowPosition, direction: direction)
        self.delegate?.loginTooltipViewWillAppear?(self)
      } else {
        self.delegate?.loginTooltipViewWillNotAppear?(self)
      }
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit
import UIKit

/**
 Tooltip bubble with text in it used to display tips for UI elements,
 with a pointed arrow (to refer to the UI element).

 The tooltip fades in and will automatically fade out. See `displayDuration`.
 */
@objc(FBSDKTooltipView)
open class FBTooltipView: UIView {

  // MARK: - Definitions

  /**
   FBSDKTooltipViewArrowDirection enum

    Passed on construction to determine arrow orientation.
   */
  @objc(FBSDKTooltipViewArrowDirection)
  @frozen public enum ArrowDirection: UInt {
    case down = 0
    // swiftlint:disable:next identifier_name
    case up = 1
  }

  /**
   FBSDKTooltipColorStyle enum

   Passed on construction to determine color styling.
   */
  @objc(FBSDKTooltipColorStyle)
  @frozen public enum ColorStyle: UInt {
    case friendlyBlue = 0
    case neutralGray = 1
  }

  /// Constants
  private enum Constants {
    static let kTransitionDuration: CGFloat = 0.3
    static let kZoomOutScale: CGFloat = 0.001
    static let kZoomInScale: CGFloat = 1.1
    static let kZoomBounceScale: CGFloat = 0.98

    static let kNUXRectInset: CGFloat = 6
    static let kNUXBubbleMargin: CGFloat = 17 - kNUXRectInset
    static let kNUXPointMargin: CGFloat = -3
    static let kNUXCornerRadius: CGFloat = 4
    static let kNUXStrokeLineWidth: CGFloat = 0.5
    static let kNUXSideCap: CGFloat = 6
    static let kNUXFontSize: CGFloat = 10
    static let kNUXCrossGlyphSize: CGFloat = 11

    static let kFriendlyBlueGradientColors: [CGColor] = [
      UIColor(red: 0x6e / 255.0, green: 0x9c / 255.0, blue: 0xf5 / 255.0, alpha: 1.0).cgColor,
      UIColor(red: 0x49 / 255.0, green: 0x74 / 255.0, blue: 0xc6 / 255.0, alpha: 1.0).cgColor,
    ]
    static let kNeutralGray: [CGColor] = [
      UIColor(red: 0x51 / 255.0, green: 0x50 / 255.0, blue: 0x4f / 255.0, alpha: 1.0).cgColor,
      UIColor(red: 0x2d / 255.0, green: 0x2c / 255.0, blue: 0x2c / 255.0, alpha: 1.0).cgColor,
    ]
  }

  // MARK: - Properties

  /**
   Gets or sets the amount of time in seconds the tooltip should be displayed.
   Set this to zero to make the display permanent until explicitly dismissed.
   Defaults to six seconds.
   */
  @objc public var displayDuration: TimeInterval = 6.0

  /**
   Gets or sets the color style after initialization.
   Defaults to value passed to -initWithTagline:message:colorStyle:.
   */
  @objc public var colorStyle: ColorStyle {
    didSet {
      updateColors()
    }
  }

  /// Gets or sets the message.
  @objc public var message: String? {
    willSet {
      guard newValue != message else {
        return
      }
      set(message: newValue, tagline: tagline)
    }
  }

  /// Gets or sets the optional phrase that comprises the first part of the label (and is highlighted differently).
  @objc public var tagline: String? {
    willSet {
      guard newValue != tagline else {
        return
      }
      set(message: message, tagline: newValue)
    }
  }

  private var positionInView: CGPoint = .zero
  private var displayTime: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()
  private let minimumDisplayDuration: CFTimeInterval = 6.0

  /// Primary text label for tooltip
  @objc let textLabel: UILabel = {
    let textLabel = UILabel(frame: .zero)
    textLabel.backgroundColor = .clear
    textLabel.autoresizingMask = .flexibleRightMargin
    textLabel.numberOfLines = 0
    textLabel.font = UIFont.boldSystemFont(ofSize: FBTooltipView.Constants.kNUXFontSize)
    textLabel.textAlignment = .left
    return textLabel
  }()

  private var insideTapGestureRecognizer: UITapGestureRecognizer?
  private var leftWidth: CGFloat = .zero
  private var rightWidth: CGFloat = .zero
  private var arrowMidpoint: CGFloat = .zero
  private var pointingUp = false
  private var isFadingOut = false

  // Style
  private var innerStrokeColor: UIColor = .clear
  private let arrowHeight: CGFloat = 7
  private let textPadding: CGFloat = 10
  private let maximumTextWidth: CGFloat = 185
  private let verticalTextOffset: CGFloat = 0
  private let verticalCrossOffset: CGFloat = -2.5
  private var gradientColors: [CGColor] = []
  private var crossCloseGlyphColor: UIColor = .clear

  // MARK: - Init

  /// Convenience constructor
  convenience init() {
    self.init(tagline: nil, message: nil, colorStyle: .friendlyBlue)
  }

  /// Designated initializer.
  /// - Parameters:
  ///   - tagline: First part of the label, that will be highlighted with different color. Can be nil.
  ///   - message: Main message to display.
  ///   - colorStyle: Color style to use for tooltip.
  ///
  /// If you need to show a tooltip for login, consider using the `FBSDKLoginTooltipView` view.
  /// See FBSDKLoginTooltipView
  @objc public init(
    tagline: String?,
    message: String?,
    colorStyle: ColorStyle
  ) {
    self.tagline = tagline
    self.message = message
    self.colorStyle = colorStyle

    super.init(frame: .zero)

    updateColors()
    set(message: message, tagline: tagline)
    addSubview(textLabel)

    // Tap gesture
    let gesture = UITapGestureRecognizer(target: self, action: #selector(onTapInTooltip(_:)))
    addGestureRecognizer(gesture)
    insideTapGestureRecognizer = gesture

    // Other Set Up
    isOpaque = false
    backgroundColor = .clear
    layer.needsDisplayOnBoundsChange = true
    layer.shadowColor = UIColor.black.cgColor
    layer.shadowOpacity = 0.5
    layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
    layer.shadowRadius = 5.0
    layer.masksToBounds = false
  }

  @available(*, unavailable)
  public required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  deinit {
    insideTapGestureRecognizer?.removeTarget(self, action: nil)
  }

  // MARK: - Presentation

  /// Show tooltip at the top or at the bottom of given view.
  /// Tooltip will be added to anchorView.window.rootViewController.view
  ///
  /// - Parameter anchorView: view to show at, must be already added to window view hierarchy, in order to decide
  /// where tooltip will be shown. (If there's not enough space at the top of the anchorView in window bounds -
  /// tooltip will be shown at the bottom of it)
  ///
  /// Use this method to present the tooltip with automatic positioning or
  /// use -presentInView:withArrowPosition:direction: for manual positioning
  /// If anchorView is nil or has no window - this method does nothing.
  @objc(presentFromView:)
  public func present(from anchorView: UIView) {
    guard let superview = anchorView.window?.rootViewController?.view else {
      return
    }

    // By default - attach to the top, pointing down
    var position = CGPoint(x: anchorView.bounds.midX, y: anchorView.bounds.minY)
    var positionInSuperview = superview.convert(position, from: anchorView)
    var direction: ArrowDirection = .down

    // If not enough space to point up from top of anchor view - point up to it's bottom
    let bubbleHeight: CGFloat = textLabel.bounds.height + verticalTextOffset + textPadding * 2
    if positionInSuperview.y - bubbleHeight - Constants.kNUXBubbleMargin < superview.bounds.minY {
      direction = .up
      position = CGPoint(x: anchorView.bounds.midX, y: anchorView.bounds.maxY)
      positionInSuperview = superview.convert(position, from: anchorView)
    }

    present(in: superview, arrowPosition: positionInSuperview, direction: direction)
  }

  /// Adds tooltip to given view, with given position and arrow direction.
  /// - Parameters:
  ///   - view: View to be used as superview.
  ///   - arrowPosition: Point in view's cordinates, where arrow will be pointing
  ///   - direction: whenever arrow should be pointing up (message bubble is below the arrow) or down (message bubble is above the arrow).
  @objc(presentInView:withArrowPosition:direction:)
  open func present(
    in view: UIView,
    arrowPosition: CGPoint,
    direction: ArrowDirection
  ) {
    pointingUp = direction == .up
    positionInView = arrowPosition
    frame = layoutSubviewsAndDetermineFrame()

    // Add to view, while invisible.
    isHidden = true
    if superview != nil {
      removeFromSuperview()
    }
    view.addSubview(self)

    // Layout & schedule dismissal.
    displayTime = CFAbsoluteTimeGetCurrent()
    isFadingOut = false
    scheduleAutomaticFadeout()
    layoutSubviews()
    animateFadeIn()
  }

  /// Remove tooltip manually.
  /// Calling this method isn't necessary - tooltip will dismiss itself automatically after the `displayDuration`.
  @objc public func dismiss() {
    guard !isFadingOut else {
      return
    }

    isFadingOut = true

    animateFadeOut { [weak self] in
      self?.removeFromSuperview()
      self?.cancelAllScheduledFadeOutMethods()
      self?.isFadingOut = false
    }
  }

  // MARK: - Private Methods

  // MARK: - Animation

  @objc(animateFadeIn)
  func animateFadeIn() {
    // Prepare Animation: Zoom in with bounce. Keep the arrow point in place.
    // Set initial transform (zoomed out) & become visible.
    let centerPos: CGFloat = bounds.size.width / 2.0
    let zoomOffsetX: CGFloat = (centerPos - arrowMidpoint) * (Constants.kZoomOutScale - 1.0)
    var zoomOffsetY: CGFloat = -0.5 * bounds.size.height * (Constants.kZoomOutScale - 1.0)
    if pointingUp {
      zoomOffsetY = -zoomOffsetY
    }
    let transformer = FBSDKTransformer()
    let zoomOutScale = transformer.caTransform3DMakeScale(
      Constants.kZoomOutScale,
      sy: Constants.kZoomOutScale,
      sz: Constants.kZoomOutScale
    )
    let zoomOffsetTranslation = transformer.caTransform3DMakeTranslation(zoomOffsetX, ty: zoomOffsetY, tz: 0)
    layer.transform = transformer.caTransform3DConcat(zoomOutScale, b: zoomOffsetTranslation)
    isHidden = false

    // Prepare animation steps
    // 1st Step.
    let zoomIn: () -> Void = {
      self.alpha = 1.0

      let newZoomOffsetX: CGFloat = (centerPos - self.arrowMidpoint) * (Constants.kZoomInScale - 1.0)
      var newZoomOffsetY: CGFloat = -0.5 * self.bounds.size.height * (Constants.kZoomInScale - 1.0)
      if self.pointingUp {
        newZoomOffsetY = -newZoomOffsetY
      }

      let zoomInScale = transformer.caTransform3DMakeScale(
        Constants.kZoomInScale,
        sy: Constants.kZoomInScale,
        sz: Constants.kZoomInScale
      )
      let newZoomOffsetTranslation = transformer.caTransform3DMakeTranslation(newZoomOffsetX, ty: newZoomOffsetY, tz: 0)
      self.layer.transform = transformer.caTransform3DConcat(zoomInScale, b: newZoomOffsetTranslation)
    }

    // 2nd Step.
    let bounceZoom = {
      let centerPos2: CGFloat = self.bounds.size.width / 2.0
      let zoomOffsetX2: CGFloat = (centerPos2 - self.arrowMidpoint) * (Constants.kZoomBounceScale - 1.0)
      var zoomOffsetY2: CGFloat = -0.5 * self.bounds.size.height * (Constants.kZoomBounceScale - 1.0)
      if self.pointingUp {
        zoomOffsetY2 = -zoomOffsetY2
      }
      let zoomBounceScale = transformer.caTransform3DMakeScale(
        Constants.kZoomBounceScale,
        sy: Constants.kZoomBounceScale,
        sz: Constants.kZoomBounceScale
      )
      let bounceOffsetTranslation = transformer.caTransform3DMakeTranslation(zoomOffsetX2, ty: zoomOffsetY2, tz: 0)
      self.layer.transform = transformer.caTransform3DConcat(zoomBounceScale, b: bounceOffsetTranslation)
    }

    // 3rd Step.
    let normalizeZoom: () -> Void = {
      self.layer.transform = FBSDKCATransform3DIdentity
    }

    // Animate 3 steps sequentially
    UIView.animate(
      withDuration: Constants.kTransitionDuration / 1.5,
      delay: 0,
      options: .curveEaseInOut,
      animations: zoomIn
    ) { _ in
      UIView.animate(
        withDuration: Constants.kTransitionDuration / 2.2,
        animations: bounceZoom
      ) { _ in
        UIView.animate(
          withDuration: Constants.kTransitionDuration / 5,
          animations: normalizeZoom
        )
      }
    }
  }

  /// Fade out tooltip with animation
  /// - Parameter completionHandler: Callback after animation
  private func animateFadeOut(completionHandler: @escaping () -> Void) {
    UIView.animate(
      withDuration: 0.3,
      delay: 0,
      options: .curveEaseInOut
    ) {
      self.alpha = 0
    } completion: { _ in
      completionHandler()
    }
  }

  // MARK: - Gestures

  /// Handle tap gesture
  /// - Parameter sender: Tap gesture reference
  @objc private func onTapInTooltip(_ sender: UIGestureRecognizer) {
    // ignore incomplete tap gestures
    guard sender.state == .ended else {
      return
    }

    // fade out the tooltip view right away
    dismiss()
  }

  // MARK: - Drawing

  /// Create up pointing tooltip bubble
  /// - Parameters:
  ///   - rect: Bubble rect
  ///   - arrowMidpoint: Arrow x anchor midPoint
  ///   - arrowHeight: Arrow size height
  ///   - radius: Bubble radius
  /// - Returns: `CGMutablePath`
  private func fbsdkCreateUpPointingBubbleWithRect(
    _ rect: CGRect,
    _ arrowMidpoint: CGFloat,
    _ arrowHeight: CGFloat,
    _ radius: CGFloat
  ) -> CGMutablePath {
    let path = CGMutablePath()
    let arrowHalfWidth: CGFloat = arrowHeight

    // start with arrow
    path.move(to: CGPoint(x: arrowMidpoint - arrowHalfWidth, y: rect.minY))
    path.addLine(to: CGPoint(x: arrowMidpoint, y: rect.minY - arrowHeight))
    path.addLine(to: CGPoint(x: arrowMidpoint + arrowHalfWidth, y: rect.minY))

    // rest of curved rectangle
    path.addArc(
      tangent1End: CGPoint(x: rect.maxX, y: rect.minY),
      tangent2End: CGPoint(x: rect.maxX, y: rect.maxY),
      radius: radius
    )
    path.addArc(
      tangent1End: CGPoint(x: rect.maxX, y: rect.maxY),
      tangent2End: CGPoint(x: rect.minX, y: rect.maxY),
      radius: radius
    )
    path.addArc(
      tangent1End: CGPoint(x: rect.minX, y: rect.maxY),
      tangent2End: CGPoint(x: rect.minX, y: rect.minY),
      radius: radius
    )
    path.addArc(
      tangent1End: CGPoint(x: rect.minX, y: rect.minY),
      tangent2End: CGPoint(x: rect.maxX, y: rect.minY),
      radius: radius
    )
    path.closeSubpath()
    return path
  }

  /// Create down pointing tooltip bubble
  /// - Parameters:
  ///   - rect: Bubble rect
  ///   - arrowMidpoint: Arrow x anchor midPoint
  ///   - arrowHeight: Arrow size height
  ///   - radius: Bubble radius
  /// - Returns: `CGMutablePath`
  private func fbsdkCreateDownPointingBubbleWithRect(
    _ rect: CGRect,
    _ arrowMidpoint: CGFloat,
    _ arrowHeight: CGFloat,
    _ radius: CGFloat
  ) -> CGMutablePath {
    let path = CGMutablePath()
    let arrowHalfWidth: CGFloat = arrowHeight

    // start with arrow
    path.move(to: CGPoint(x: arrowMidpoint + arrowHalfWidth, y: rect.maxY))
    path.addLine(to: CGPoint(x: arrowMidpoint, y: rect.maxY + arrowHeight))
    path.addLine(to: CGPoint(x: arrowMidpoint - arrowHalfWidth, y: rect.maxY))

    // rest of curved rectangle
    path.addArc(
      tangent1End: CGPoint(x: rect.minX, y: rect.maxY),
      tangent2End: CGPoint(x: rect.minX, y: rect.minY),
      radius: radius
    )
    path.addArc(
      tangent1End: CGPoint(x: rect.minX, y: rect.minY),
      tangent2End: CGPoint(x: rect.maxX, y: rect.minY),
      radius: radius
    )
    path.addArc(
      tangent1End: CGPoint(x: rect.maxX, y: rect.minY),
      tangent2End: CGPoint(x: rect.maxX, y: rect.maxY),
      radius: radius
    )
    path.addArc(
      tangent1End: CGPoint(x: rect.maxX, y: rect.maxY),
      tangent2End: CGPoint(x: rect.minX, y: rect.maxY),
      radius: radius
    )
    path.closeSubpath()
    return path
  }

  /// Create path for close icon
  /// - Parameter rect: Rect to draw in
  /// - Returns: `CGMutablePath`
  private func createCloseCrossGlyphWithRect(_ rect: CGRect) -> CGMutablePath {
    let lineThickness: CGFloat = 0.20 * rect.height

    // One rectangle
    let path1 = CGMutablePath()
    path1.move(to: CGPoint(x: rect.minX, y: rect.minY + lineThickness))
    path1.addLine(to: CGPoint(x: rect.minX + lineThickness, y: rect.minY))
    path1.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - lineThickness))
    path1.addLine(to: CGPoint(x: rect.maxX - lineThickness, y: rect.maxY))
    path1.closeSubpath()

    // 2nd rectangle - mirrored horizontally
    let path2 = CGMutablePath()
    path2.move(to: CGPoint(x: rect.minX, y: rect.maxY - lineThickness))
    path2.addLine(to: CGPoint(x: rect.maxX - lineThickness, y: rect.minY))
    path2.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + lineThickness))
    path2.addLine(to: CGPoint(x: rect.minX + lineThickness, y: rect.maxY))
    path2.closeSubpath()

    let groupedPath = CGMutablePath()
    groupedPath.addPath(path1)
    groupedPath.addPath(path2)

    return groupedPath
  }

  public override func draw(_ rect: CGRect) {
    // Ignore dirty rect and just redraw the entire nux bubble
    let arrowSideMargin: CGFloat = 1 + 0.5 * max(Constants.kNUXRectInset, arrowHeight)
    let arrowYMarginOffset: CGFloat = pointingUp ? arrowSideMargin : Constants.kNUXRectInset
    let halfStroke: CGFloat = Constants.kNUXStrokeLineWidth / 2.0
    let outerRect = CGRect(
      x: Constants.kNUXRectInset + halfStroke,
      y: arrowYMarginOffset + halfStroke,
      width: bounds.size.width - 2 * Constants.kNUXRectInset - Constants.kNUXStrokeLineWidth,
      height: bounds.size.height - Constants.kNUXRectInset - arrowSideMargin - Constants.kNUXStrokeLineWidth
    ).insetBy(dx: 5, dy: 5)

    let innerRect: CGRect = outerRect.insetBy(dx: Constants.kNUXStrokeLineWidth, dy: Constants.kNUXStrokeLineWidth)
    let fillRect: CGRect = innerRect.insetBy(
      dx: Constants.kNUXStrokeLineWidth / 2.0,
      dy: Constants.kNUXStrokeLineWidth / 2.0
    )
    let closeCrossGlyphPositionY: CGFloat = min(
      fillRect.minY + textPadding + verticalCrossOffset,
      fillRect.midY - 0.5 * Constants.kNUXCrossGlyphSize
    )
    let closeCrossGlyphRect = CGRect(
      x: fillRect.maxX - 2 * Constants.kNUXFontSize,
      y: closeCrossGlyphPositionY,
      width: Constants.kNUXCrossGlyphSize,
      height: Constants.kNUXCrossGlyphSize
    )

    // setup and get paths
    guard let context = UIGraphicsGetCurrentContext() else {
      return
    }

    let outerPath: CGMutablePath
    let innerPath: CGMutablePath
    let fillPath: CGMutablePath
    let crossCloseGlyphPath = createCloseCrossGlyphWithRect(closeCrossGlyphRect)
    var gradientRect: CGRect = fillRect
    if pointingUp {
      outerPath = fbsdkCreateUpPointingBubbleWithRect(
        outerRect,
        arrowMidpoint,
        arrowHeight,
        Constants.kNUXCornerRadius + Constants.kNUXStrokeLineWidth
      )
      innerPath = fbsdkCreateUpPointingBubbleWithRect(
        innerRect,
        arrowMidpoint,
        arrowHeight,
        Constants.kNUXCornerRadius
      )
      fillPath = fbsdkCreateUpPointingBubbleWithRect(
        fillRect,
        arrowMidpoint,
        arrowHeight,
        Constants.kNUXCornerRadius - Constants.kNUXStrokeLineWidth
      )
      gradientRect.origin.y -= arrowHeight
      gradientRect.size.height += arrowHeight
    } else {
      outerPath = fbsdkCreateDownPointingBubbleWithRect(
        outerRect,
        arrowMidpoint,
        arrowHeight,
        Constants.kNUXCornerRadius + Constants.kNUXStrokeLineWidth
      )
      innerPath = fbsdkCreateDownPointingBubbleWithRect(
        innerRect,
        arrowMidpoint,
        arrowHeight,
        Constants.kNUXCornerRadius
      )
      fillPath = fbsdkCreateDownPointingBubbleWithRect(
        fillRect,
        arrowMidpoint,
        arrowHeight,
        Constants.kNUXCornerRadius - Constants.kNUXStrokeLineWidth
      )
      gradientRect.size.height += arrowHeight
    }
    layer.shadowPath = outerPath

    // This tooltip has two borders, so draw two strokes and a fill.
    let strokeColor = innerStrokeColor.cgColor
    context.saveGState()
    context.setStrokeColor(strokeColor)
    context.setLineWidth(Constants.kNUXStrokeLineWidth)
    context.addPath(innerPath)
    context.strokePath()
    context.addPath(fillPath)
    context.clip()

    let rgbColorspace = CGColorSpaceCreateDeviceRGB()
    guard let gradient = CGGradient(
      colorsSpace: rgbColorspace,
      colors: gradientColors as CFArray,
      locations: nil
    ) else {
      return
    }

    let start = CGPoint(x: gradientRect.origin.x, y: gradientRect.origin.y)
    let end = CGPoint(x: gradientRect.origin.x, y: gradientRect.maxY)
    context.drawLinearGradient(gradient, start: start, end: end, options: .init(rawValue: 0))
    context.addPath(crossCloseGlyphPath)
    context.setFillColor(crossCloseGlyphColor.cgColor)
    context.fillPath()
    context.restoreGState()
  }

  /// Update style based colors
  private func updateColors() {
    switch colorStyle {
    case .neutralGray:
      gradientColors = Constants.kNeutralGray
      innerStrokeColor = UIColor(white: 0.13, alpha: 1)
      crossCloseGlyphColor = UIColor(white: 0.69, alpha: 1)

    case .friendlyBlue:
      gradientColors = Constants.kFriendlyBlueGradientColors
      innerStrokeColor = UIColor(red: 0.12, green: 0.26, blue: 0.55, alpha: 1.0)
      crossCloseGlyphColor = UIColor(red: 0.60, green: 0.73, blue: 1.0, alpha: 1.0)
    }

    textLabel.textColor = .white
  }

  // MARK: - Layout

  public override func layoutSubviews() {
    super.layoutSubviews()
    // We won't set the frame in layoutSubviews to avoid potential infinite loops.
    // Frame is set in -presentInView:withArrowPosition:direction: method.
    _ = layoutSubviewsAndDetermineFrame()
  }

  private func layoutSubviewsAndDetermineFrame() -> CGRect {
    // Compute the positioning of the arrow.
    var screenBounds: CGRect = UIScreen.main.bounds
    let orientation: UIInterfaceOrientation = UIApplication.shared.statusBarOrientation
    if !orientation.isPortrait {
      screenBounds = CGRect(x: 0, y: 0, width: screenBounds.size.height, height: screenBounds.size.width)
    }
    let arrowHalfWidth: CGFloat = arrowHeight
    var arrowXPos: CGFloat = positionInView.x - arrowHalfWidth
    arrowXPos = max(arrowXPos, Constants.kNUXSideCap + Constants.kNUXBubbleMargin)
    arrowXPos = min(
      arrowXPos,
      screenBounds.size.width - Constants.kNUXBubbleMargin - Constants.kNUXSideCap - 2 * arrowHalfWidth
    )
    positionInView = CGPoint(x: arrowXPos + arrowHalfWidth, y: positionInView.y)

    let arrowYMarginOffset: CGFloat = pointingUp ? max(Constants.kNUXRectInset, arrowHeight) : Constants.kNUXRectInset

    // Set the lock image frame.
    let xPos: CGFloat = Constants.kNUXRectInset + textPadding + Constants.kNUXStrokeLineWidth
    let yPos: CGFloat = arrowYMarginOffset + Constants.kNUXStrokeLineWidth + textPadding

    // Set the text label frame.
    textLabel.frame = CGRect(
      x: xPos,
      y: yPos + verticalTextOffset, // sizing function may not return desired height exactly
      width: textLabel.bounds.width,
      height: textLabel.bounds.height
    )

    // Determine the size of the nux bubble.
    let bubbleHeight = textLabel.bounds.height + verticalTextOffset + textPadding * 2
    let crossGlyphWidth = 2 * Constants.kNUXFontSize
    let bubbleWidth = textLabel.bounds.width + textPadding * 2 + Constants.kNUXStrokeLineWidth * 2 + crossGlyphWidth

    // Compute the widths to the left and right of the arrow.
    leftWidth = CGFloat(roundf(0.5 * Float(bubbleWidth - 2 * arrowHalfWidth)))
    rightWidth = leftWidth

    var originX: CGFloat = arrowXPos - leftWidth
    if originX < Constants.kNUXBubbleMargin {
      let xShift: CGFloat = Constants.kNUXBubbleMargin - originX
      originX += xShift
      leftWidth -= xShift
      rightWidth += xShift
    } else if originX + bubbleWidth > screenBounds.size.width - Constants.kNUXBubbleMargin {
      let xShift: CGFloat = originX + bubbleWidth - (screenBounds.size.width - Constants.kNUXBubbleMargin)
      originX -= xShift
      leftWidth += xShift
      rightWidth -= xShift
    }

    arrowMidpoint = positionInView.x - originX + Constants.kNUXRectInset

    // Set the frame for the view.
    let nuxWidth: CGFloat = bubbleWidth + 2 * Constants.kNUXRectInset
    let nuxHeight: CGFloat = bubbleHeight + Constants.kNUXRectInset + max(
      Constants.kNUXRectInset,
      arrowHeight
    ) + 2 * Constants.kNUXStrokeLineWidth
    var yOrigin: CGFloat = 0
    if pointingUp {
      yOrigin = positionInView.y + Constants.kNUXPointMargin - max(0, Constants.kNUXRectInset - arrowHeight)
    } else {
      yOrigin = positionInView.y - nuxHeight - Constants.kNUXPointMargin + max(0, Constants.kNUXRectInset - arrowHeight)
    }

    return CGRect(
      x: originX - Constants.kNUXRectInset,
      y: yOrigin,
      width: nuxWidth,
      height: nuxHeight
    )
  }

  // MARK: - Message & Tagline

  /// Set tooltip content
  /// - Parameters:
  ///   - message: Nullable message text
  ///   - tagline: Nullable tagline text
  private func set(message: String?, tagline: String?) {
    var kmessage = message ?? ""
    // Ensure tagline is empty string or ends with space
    var ktagline = tagline ?? ""
    if !ktagline.isEmpty, !ktagline.hasSuffix(" ") {
      ktagline = "\(ktagline) "
    }

    // Concatenate tagline & main message
    kmessage = ktagline + kmessage

    let fullRange = NSRange(location: 0, length: kmessage.count)
    let attrString = NSMutableAttributedString(string: kmessage)
    let font = UIFont.boldSystemFont(ofSize: Constants.kNUXFontSize)

    attrString.addAttribute(.font, value: font, range: fullRange)
    attrString.addAttribute(.foregroundColor, value: UIColor.white, range: fullRange)

    if !ktagline.isEmpty {
      let color = UIColor(
        red: 0x6D / 255.0,
        green: 0x87 / 255.0,
        blue: 0xC7 / 255.0,
        alpha: 1
      )
      attrString.addAttribute(.foregroundColor, value: color, range: NSRange(location: 0, length: ktagline.count))
    }

    textLabel.attributedText = attrString

    let textLabelSize: CGSize = textLabel.sizeThatFits(
      CGSize(
        width: maximumTextWidth,
        height: .greatestFiniteMagnitude
      )
    )
    textLabel.bounds = CGRect(x: 0, y: 0, width: textLabelSize.width, height: textLabelSize.height)
    frame = layoutSubviewsAndDetermineFrame()
    setNeedsDisplay()
  }

  // MARK: - Auto Dismiss Timeout

  /// Schedule view fade out
  private func scheduleAutomaticFadeout() {
    Self.cancelPreviousPerformRequests(
      withTarget: self,
      selector: #selector(scheduleFadeoutRespectingMinimumDisplayDuration),
      object: nil
    )

    if displayDuration > 0.0, superview != nil {
      let intervalAlreadyDisplaying: CFTimeInterval = CFAbsoluteTimeGetCurrent() - displayTime
      let timeRemainingBeforeAutomaticFadeout: CFTimeInterval = displayDuration - intervalAlreadyDisplaying
      if timeRemainingBeforeAutomaticFadeout > 0.0 {
        perform(
          #selector(scheduleFadeoutRespectingMinimumDisplayDuration),
          with: nil,
          afterDelay: timeRemainingBeforeAutomaticFadeout
        )
      } else {
        scheduleFadeoutRespectingMinimumDisplayDuration()
      }
    }
  }

  /// Schedules faded out while respecting minimum display duration
  @objc private func scheduleFadeoutRespectingMinimumDisplayDuration() {
    let intervalAlreadyDisplaying: CFTimeInterval = CFAbsoluteTimeGetCurrent() - displayTime
    let remainingDisplayTime: CFTimeInterval = minimumDisplayDuration - intervalAlreadyDisplaying
    if remainingDisplayTime > 0.0 {
      perform(#selector(dismiss), with: nil, afterDelay: remainingDisplayTime)
    } else {
      dismiss()
    }
  }

  /// Cancel scheduled methods (selectors schedule via `perform(:)` API)
  private func cancelAllScheduledFadeOutMethods() {
    Self.cancelPreviousPerformRequests(
      withTarget: self,
      selector: #selector(scheduleFadeoutRespectingMinimumDisplayDuration),
      object: nil
    )
    Self.cancelPreviousPerformRequests(withTarget: self, selector: #selector(dismiss), object: nil)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit

extension AppEvents.Name {
  static let loginButtonDidTap = AppEvents.Name("fb_login_button_did_tap")

  // MARK: - Device Requests

  static let smartLoginService = AppEvents.Name("fb_smart_login_service")

  // MARK: - Login Manager

  /// Use to log the start of an auth request that cannot be fulfilled by the token cache
  static let sessionAuthStart = AppEvents.Name("fb_mobile_login_start")

  /// Use to log the end of an auth request that was not fulfilled by the token cache
  static let sessionAuthEnd = AppEvents.Name("fb_mobile_login_complete")

  /// Use to log the start of a specific auth method as part of an auth request
  static let sessionAuthMethodStart = AppEvents.Name("fb_mobile_login_method_start")

  /// Use to log the end of the last tried auth method as part of an auth request
  static let sessionAuthMethodEnd = AppEvents.Name("fb_mobile_login_method_complete")

  /// Use to log the post-login heartbeat event after  the end of an auth request
  static let sessionAuthHeartbeat = AppEvents.Name("fb_mobile_login_heartbeat")
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

/**
 A delegate for `FBSDKLoginButton`
 */
@objc(FBSDKLoginButtonDelegate)
public protocol LoginButtonDelegate: NSObjectProtocol {

  /**
   Sent to the delegate when the button was used to login.
   @param loginButton The button being used to log in
   @param result The results of the login
   @param error The error (if any) from the login
   */
  @objc(loginButton:didCompleteWithResult:error:)
  func loginButton(_ loginButton: FBLoginButton, didCompleteWith result: LoginManagerLoginResult?, error: Error?)

  /**
   Sent to the delegate when the button was used to logout.
   @param loginButton The button being used to log out.
   */
  @objc
  func loginButtonDidLogOut(_ loginButton: FBLoginButton)

  /**
   Sent to the delegate when the button is about to login.
   @param loginButton The button being used to log in
   @return `true` if the login should be allowed to proceed, `false` otherwise
   */
  @objc
  optional func loginButtonWillLogin(_ loginButton: FBLoginButton) -> Bool
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit
import Foundation

/// A configuration to use for modifying the behavior of a login attempt.
@objcMembers
@objc(FBSDKLoginConfiguration)
public final class LoginConfiguration: NSObject {

  /// The nonce that the configuration was created with.
  /// A unique nonce will be used if none is provided to the initializer.
  public let nonce: String

  /// The tracking  preference. Defaults to `.enabled`.
  public let tracking: LoginTracking

  /// The requested permissions for the login attempt. Defaults to an empty set.
  public let requestedPermissions: Set<FBPermission>

  /// The Messenger Page Id associated with this login request.
  public let messengerPageId: String?

  /// The auth type associated with this login request.
  public let authType: LoginAuthType?

  /// The code verifier used in the PKCE process.
  /// If not provided, a code verifier will be randomly generated.
  public let codeVerifier: CodeVerifier

  /**
   Attempts to initialize a new configuration with the expected parameters.

   @param permissions the requested permissions for a login attempt. Permissions must be an array of strings that do not contain whitespace.
   @param tracking the tracking preference to use for a login attempt.
   @param nonce an optional nonce to use for the login attempt. A valid nonce must be a non-empty string without whitespace.
   Creation of the configuration will fail if the nonce is invalid.
   @param messengerPageId the associated page id  to use for a login attempt.
   */
  @objc(initWithPermissions:tracking:nonce:messengerPageId:)
  public convenience init?(
    permissions: [String],
    tracking: LoginTracking,
    nonce: String,
    messengerPageId: String?
  ) {
    self.init(
      permissions: permissions,
      tracking: tracking,
      nonce: nonce,
      messengerPageId: messengerPageId,
      authType: .rerequest
    )
  }

  /**
   Attempts to initialize a new configuration with the expected parameters.

   @param permissions the requested permissions for a login attempt. Permissions must be an array of strings that do not contain whitespace.
   @param tracking the tracking preference to use for a login attempt.
   @param nonce an optional nonce to use for the login attempt. A valid nonce must be a non-empty string without whitespace.
   Creation of the configuration will fail if the nonce is invalid.
   @param messengerPageId the associated page id  to use for a login attempt.
   @param authType auth_type param to use for login.
   */
  @objc(initWithPermissions:tracking:nonce:messengerPageId:authType:)
  public convenience init?(
    permissions: [String],
    tracking: LoginTracking,
    nonce: String,
    messengerPageId: String?,
    authType: LoginAuthType?
  ) {
    self.init(
      permissions: permissions,
      tracking: tracking,
      nonce: nonce,
      messengerPageId: messengerPageId,
      authType: authType,
      codeVerifier: CodeVerifier()
    )
  }

  /**
   Attempts to initialize a new configuration with the expected parameters.

   @param permissions the requested permissions for a login attempt. Permissions must be an array of strings that do not contain whitespace.
   @param tracking the tracking preference to use for a login attempt.
   @param nonce an optional nonce to use for the login attempt. A valid nonce must be a non-empty string without whitespace.
   Creation of the configuration will fail if the nonce is invalid.
   */
  @objc(initWithPermissions:tracking:nonce:)
  public convenience init?(
    permissions: [String],
    tracking: LoginTracking,
    nonce: String
  ) {
    self.init(
      permissions: permissions,
      tracking: tracking,
      nonce: nonce,
      messengerPageId: nil
    )
  }

  /**
   Attempts to initialize a new configuration with the expected parameters.

   @param permissions the requested permissions for the login attempt. Permissions must be an array of strings that do not contain whitespace.
   @param tracking the tracking preference to use for a login attempt.
   @param messengerPageId the associated page id  to use for a login attempt.
   */
  @objc(initWithPermissions:tracking:messengerPageId:)
  public convenience init?(
    permissions: [String],
    tracking: LoginTracking,
    messengerPageId: String?
  ) {
    self.init(
      permissions: permissions,
      tracking: tracking,
      nonce: UUID().uuidString,
      messengerPageId: messengerPageId
    )
  }

  /**
   Attempts to initialize a new configuration with the expected parameters.

   @param permissions the requested permissions for the login attempt. Permissions must be an array of strings that do not contain whitespace.
   @param tracking the tracking preference to use for a login attempt.
   @param messengerPageId the associated page id  to use for a login attempt.
   @param authType auth_type param to use for login.
   */
  @objc(initWithPermissions:tracking:messengerPageId:authType:)
  public convenience init?(
    permissions: [String],
    tracking: LoginTracking,
    messengerPageId: String?,
    authType: LoginAuthType?
  ) {
    self.init(
      permissions: permissions,
      tracking: tracking,
      nonce: UUID().uuidString,
      messengerPageId: messengerPageId,
      authType: authType
    )
  }

  /**
   Attempts to initialize a new configuration with the expected parameters.

   @param permissions the requested permissions for a login attempt. Permissions must be an array of strings that do not contain whitespace.
   @param tracking the tracking preference to use for a login attempt.
   @param nonce an optional nonce to use for the login attempt. A valid nonce must be a non-empty string without whitespace.
   Creation of the configuration will fail if the nonce is invalid.
   @param messengerPageId the associated page id  to use for a login attempt.
   @param authType auth_type param to use for login.
   @param codeVerifier The code verifier used in the PKCE process.
   */
  @objc(initWithPermissions:tracking:nonce:messengerPageId:authType:codeVerifier:)
  public init?(
    permissions: [String],
    tracking: LoginTracking,
    nonce: String,
    messengerPageId: String?,
    authType: LoginAuthType?,
    codeVerifier: CodeVerifier
  ) {

    guard NonceValidator.isValid(nonce: nonce) else {
      let message = "Invalid nonce:\(nonce) provided to login configuration. Returning nil"
      _Logger.singleShotLogEntry(.developerErrors, logEntry: message)
      return nil
    }

    guard
      let permissions = FBPermission.permissions(fromRawPermissions: Set(permissions))
    else {
      let message = "Invalid combination of permissions provided to login configuration."
      _Logger.singleShotLogEntry(.developerErrors, logEntry: message)
      return nil
    }

    if let authType = authType,
       ![.rerequest, .reauthorize].contains(authType) {
      let message = "Invalid auth_type provided to login configuration."
      _Logger.singleShotLogEntry(.developerErrors, logEntry: message)
      return nil
    }

    requestedPermissions = permissions
    self.tracking = tracking
    self.nonce = nonce
    self.messengerPageId = messengerPageId
    self.authType = authType
    self.codeVerifier = codeVerifier
    super.init()
  }

  /**
   Attempts to initialize a new configuration with the expected parameters.

   @param permissions the requested permissions for the login attempt. Permissions must be an array of strings that do not contain whitespace.
   @param tracking the tracking preference to use for a login attempt.
   */
  @objc(initWithPermissions:tracking:)
  public convenience init?(
    permissions: [String],
    tracking: LoginTracking
  ) {
    self.init(
      permissions: permissions,
      tracking: tracking,
      nonce: UUID().uuidString
    )
  }

  /**
   Attempts to initialize a new configuration with the expected parameters.

   @param tracking the login tracking preference to use for a login attempt.
   */
  @objc(initWithTracking:)
  public convenience init?(tracking: LoginTracking) {
    self.init(
      permissions: [],
      tracking: tracking
    )
  }

  /**
   Attempts to allocate and initialize a new configuration with the expected parameters.

   - parameter permissions: The requested permissions for the login attempt.
   Defaults to an empty `Permission` array.
   - parameter tracking: The tracking preference to use for a login attempt. Defaults to `.enabled`
   - parameter nonce: An optional nonce to use for the login attempt.
    A valid nonce must be an alphanumeric string without whitespace.
    Creation of the configuration will fail if the nonce is invalid. Defaults to a `UUID` string.
   - parameter messengerPageId: An optional page id to use for a login attempt. Defaults to `nil`
   - parameter authType: An optional auth type to use for a login attempt. Defaults to `.rerequest`
   - parameter codeVerifier: An optional codeVerifier used for the PKCE process.
   If not provided, this will be randomly generated.
   */
  public convenience init?(
    permissions: Set<Permission> = [],
    tracking: LoginTracking = .enabled,
    nonce: String = UUID().uuidString,
    messengerPageId: String? = nil,
    authType: LoginAuthType? = .rerequest,
    codeVerifier: CodeVerifier = CodeVerifier()
  ) {
    self.init(
      permissions: permissions.map { $0.name },
      tracking: tracking,
      nonce: nonce,
      messengerPageId: messengerPageId,
      authType: authType,
      codeVerifier: codeVerifier
    )
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

/// Custom error type for login errors in the login error domain
public struct LoginError: CustomNSError, Hashable {

  let _nsError: NSError? // swiftlint:disable:this identifier_name

  public let errorCode: Int

  public let errorUserInfo: [String: Any]

  public init(_nsError nsError: NSError) {
    _nsError = nsError
    errorCode = nsError.code
    errorUserInfo = nsError.userInfo
  }

  public init(_ code: Code, userInfo: [String: Any] = [:]) {
    _nsError = nil
    errorCode = code.rawValue
    errorUserInfo = userInfo
  }

  public static var errorDomain: String { LoginErrorDomain }

  public typealias Code = LoginErrorCode

  /// Reserved
  public static var reserved: Code { .reserved }

  /// The error code for unknown errors
  public static var unknown: Code { .unknown }

  /// The user's password has changed and must log in again
  public static var passwordChanged: Code { .passwordChanged }

  /// The user must log in to their account on www.facebook.com to restore access
  public static var userCheckpointed: Code { .userCheckpointed }

  /// Indicates a failure to request new permissions because the user has changed
  public static var userMismatch: Code { .userMismatch }

  /// The user must confirm their account with Facebook before logging in
  public static var unconfirmedUser: Code { .unconfirmedUser }

  /// The Accounts framework failed without returning an error, indicating the app's slider in the
  /// iOS Facebook Settings (device Settings -> Facebook -> App Name) has been disabled.
  public static var systemAccountAppDisabled: Code { .systemAccountAppDisabled }

  /// An error occurred related to Facebook system Account store
  public static var systemAccountUnavailable: Code { .systemAccountUnavailable }

  /// The login response was missing a valid challenge string
  public static var badChallengeString: Code { .badChallengeString }

  /// The ID token returned in login response was invalid
  public static var invalidIDToken: Code { .invalidIDToken }

  /// A current access token was required and not provided
  public static var missingAccessToken: Code { .missingAccessToken }

  public static func == (lhs: Self, rhs: Self) -> Bool {
    if let leftError = lhs._nsError,
       let rightError = rhs._nsError {
      return leftError === rightError
    } else {
      return lhs.errorCode == rhs.errorCode
    }
  }

  public func hash(into hasher: inout Hasher) {
    hasher.combine(errorCode)
  }
}

/// Custom error codes for login errors in the login error domain
@objc(FBSDKLoginError) public enum LoginErrorCode: Int, @unchecked Sendable, Equatable {

  public typealias _ErrorType = LoginError

  /// Reserved
  case reserved = 300

  /// The error code for unknown errors
  case unknown

  /// The user's password has changed and must log in again
  case passwordChanged

  /// The user must log in to their account on www.facebook.com to restore access
  case userCheckpointed

  /// Indicates a failure to request new permissions because the user has changed
  case userMismatch

  /// The user must confirm their account with Facebook before logging in
  case unconfirmedUser

  /// The Accounts framework failed without returning an error, indicating the app's slider in the
  /// iOS Facebook Settings (device Settings -> Facebook -> App Name) has been disabled.
  case systemAccountAppDisabled

  /// An error occurred related to Facebook system Account store
  case systemAccountUnavailable

  /// The login response was missing a valid challenge string
  case badChallengeString

  /// The ID token returned in login response was invalid
  case invalidIDToken

  /// A current access token was required and not provided
  case missingAccessToken
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

/// The error domain for all errors from LoginKit
/// Error codes from the SDK in the range 300-399 are reserved for login errors in this domain.
/// Error codes from the SDK in the range 1349100-1349199 are reserved for device login errors in this domain.
public let LoginErrorDomain = FBSDKLoginErrorDomain // swiftlint:disable:this identifier_name