mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-22 00:17:32 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
425
mobile/ios/Account/FirefoxAccount.swift
Normal file
425
mobile/ios/Account/FirefoxAccount.swift
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import Shared
|
||||
import XCGLogger
|
||||
import Deferred
|
||||
import SwiftyJSON
|
||||
import FxA
|
||||
import SDWebImage
|
||||
|
||||
private let log = Logger.syncLogger
|
||||
|
||||
// The version of the account schema we persist.
|
||||
let AccountSchemaVersion = 2
|
||||
|
||||
/// A FirefoxAccount mediates access to identity attached services.
|
||||
///
|
||||
/// All data maintained as part of the account or its state should be
|
||||
/// considered sensitive and stored appropriately. Usually, that means
|
||||
/// storing account data in the iOS keychain.
|
||||
///
|
||||
/// Non-sensitive but persistent data should be maintained outside of
|
||||
/// the account itself.
|
||||
open class FirefoxAccount {
|
||||
/// The email address identifying the account. A Firefox Account is uniquely identified on a particular server
|
||||
/// (auth endpoint) by its email address.
|
||||
open let email: String
|
||||
|
||||
/// The auth endpoint user identifier identifying the account. A Firefox Account is uniquely identified on a
|
||||
/// particular server (auth endpoint) by its assigned uid.
|
||||
open let uid: String
|
||||
|
||||
open var fxaProfile: FxAProfile?
|
||||
|
||||
open var deviceRegistration: FxADeviceRegistration?
|
||||
|
||||
open var configuration: FirefoxAccountConfiguration
|
||||
|
||||
open var pushRegistration: PushRegistration?
|
||||
|
||||
fileprivate let stateCache: KeychainCache<FxAState>
|
||||
open var syncAuthState: SyncAuthState! // We can't give a reference to self if this is a let.
|
||||
|
||||
// To prevent advance() consumers racing, we maintain a shared advance() deferred (`advanceDeferred`). If an
|
||||
// advance() is in progress, the shared deferred will be returned. (Multiple consumers can chain off a single
|
||||
// deferred safely.) If no advance() is in progress, a new shared deferred will be scheduled and returned. To
|
||||
// prevent data races against the shared deferred, advance() locks accesses to `advanceDeferred` using
|
||||
// `advanceLock`.
|
||||
fileprivate var advanceLock = OSSpinLock()
|
||||
fileprivate var advanceDeferred: Deferred<FxAState>?
|
||||
|
||||
open var actionNeeded: FxAActionNeeded {
|
||||
return stateCache.value!.actionNeeded
|
||||
}
|
||||
|
||||
public convenience init(configuration: FirefoxAccountConfiguration, email: String, uid: String, deviceRegistration: FxADeviceRegistration?, stateKeyLabel: String, state: FxAState) {
|
||||
self.init(configuration: configuration, email: email, uid: uid, deviceRegistration: deviceRegistration, stateCache: KeychainCache(branch: "account.state", label: stateKeyLabel, value: state))
|
||||
}
|
||||
|
||||
public init(configuration: FirefoxAccountConfiguration, email: String, uid: String, deviceRegistration: FxADeviceRegistration?, stateCache: KeychainCache<FxAState>) {
|
||||
self.email = email
|
||||
self.uid = uid
|
||||
self.deviceRegistration = deviceRegistration
|
||||
self.configuration = configuration
|
||||
self.stateCache = stateCache
|
||||
self.stateCache.checkpoint()
|
||||
self.fxaProfile = nil
|
||||
self.syncAuthState = FirefoxAccountSyncAuthState(account: self,
|
||||
cache: KeychainCache.fromBranch("account.syncAuthState", withLabel: self.stateCache.label, factory: syncAuthStateCachefromJSON))
|
||||
}
|
||||
|
||||
open class func from(_ configuration: FirefoxAccountConfiguration, andJSON data: JSON) -> FirefoxAccount? {
|
||||
guard let email = data["email"].string ,
|
||||
let uid = data["uid"].string,
|
||||
let sessionToken = data["sessionToken"].string?.hexDecodedData,
|
||||
let keyFetchToken = data["keyFetchToken"].string?.hexDecodedData,
|
||||
let unwrapkB = data["unwrapBKey"].string?.hexDecodedData else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let verified = data["verified"].bool ?? false
|
||||
return FirefoxAccount.from(configuration: configuration,
|
||||
andParametersWithEmail: email, uid: uid, deviceRegistration: nil, verified: verified,
|
||||
sessionToken: sessionToken, keyFetchToken: keyFetchToken, unwrapkB: unwrapkB)
|
||||
}
|
||||
|
||||
open class func from(_ configuration: FirefoxAccountConfiguration,
|
||||
andLoginResponse response: FxALoginResponse,
|
||||
unwrapkB: Data) -> FirefoxAccount {
|
||||
return FirefoxAccount.from(configuration: configuration,
|
||||
andParametersWithEmail: response.remoteEmail, uid: response.uid, deviceRegistration: nil, verified: response.verified,
|
||||
sessionToken: response.sessionToken as Data, keyFetchToken: response.keyFetchToken as Data, unwrapkB: unwrapkB)
|
||||
}
|
||||
|
||||
fileprivate class func from(configuration: FirefoxAccountConfiguration,
|
||||
andParametersWithEmail email: String,
|
||||
uid: String,
|
||||
deviceRegistration: FxADeviceRegistration?,
|
||||
verified: Bool,
|
||||
sessionToken: Data,
|
||||
keyFetchToken: Data,
|
||||
unwrapkB: Data) -> FirefoxAccount {
|
||||
var state: FxAState! = nil
|
||||
if !verified {
|
||||
let now = Date.now()
|
||||
state = EngagedBeforeVerifiedState(knownUnverifiedAt: now,
|
||||
lastNotifiedUserAt: now,
|
||||
sessionToken: sessionToken,
|
||||
keyFetchToken: keyFetchToken,
|
||||
unwrapkB: unwrapkB
|
||||
)
|
||||
} else {
|
||||
state = EngagedAfterVerifiedState(
|
||||
sessionToken: sessionToken,
|
||||
keyFetchToken: keyFetchToken,
|
||||
unwrapkB: unwrapkB
|
||||
)
|
||||
}
|
||||
|
||||
let account = FirefoxAccount(
|
||||
configuration: configuration,
|
||||
email: email,
|
||||
uid: uid,
|
||||
deviceRegistration: deviceRegistration,
|
||||
stateKeyLabel: Bytes.generateGUID(),
|
||||
state: state
|
||||
)
|
||||
return account
|
||||
}
|
||||
|
||||
open func dictionary() -> [String: Any] {
|
||||
var dict: [String: Any] = [:]
|
||||
dict["version"] = AccountSchemaVersion
|
||||
dict["email"] = email
|
||||
dict["uid"] = uid
|
||||
dict["deviceRegistration"] = deviceRegistration
|
||||
dict["pushRegistration"] = pushRegistration
|
||||
dict["configurationLabel"] = configuration.label.rawValue
|
||||
dict["stateKeyLabel"] = stateCache.label
|
||||
return dict
|
||||
}
|
||||
|
||||
open class func fromDictionary(_ dictionary: [String: Any]) -> FirefoxAccount? {
|
||||
if let version = dictionary["version"] as? Int {
|
||||
// As of this writing, the current version, v2, is backward compatible with v1. The only
|
||||
// field added is pushRegistration, which is ok to be nil. If it is nil, then the app
|
||||
// will attempt registration when it starts up.
|
||||
if version <= AccountSchemaVersion {
|
||||
return FirefoxAccount.fromDictionaryV1(dictionary)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate class func fromDictionaryV1(_ dictionary: [String: Any]) -> FirefoxAccount? {
|
||||
var configurationLabel: FirefoxAccountConfigurationLabel? = nil
|
||||
if let rawValue = dictionary["configurationLabel"] as? String {
|
||||
configurationLabel = FirefoxAccountConfigurationLabel(rawValue: rawValue)
|
||||
}
|
||||
if let
|
||||
configurationLabel = configurationLabel,
|
||||
let email = dictionary["email"] as? String,
|
||||
let uid = dictionary["uid"] as? String {
|
||||
let deviceRegistration = dictionary["deviceRegistration"] as? FxADeviceRegistration
|
||||
let stateCache = KeychainCache.fromBranch("account.state", withLabel: dictionary["stateKeyLabel"] as? String, withDefault: SeparatedState(), factory: state)
|
||||
let account = FirefoxAccount(
|
||||
configuration: configurationLabel.toConfiguration(),
|
||||
email: email, uid: uid,
|
||||
deviceRegistration: deviceRegistration,
|
||||
stateCache: stateCache)
|
||||
account.pushRegistration = dictionary["pushRegistration"] as? PushRegistration
|
||||
return account
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public enum AccountError: MaybeErrorType {
|
||||
case notMarried
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .notMarried: return "Not married."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class NotATokenStateError: MaybeErrorType {
|
||||
let state: FxAState?
|
||||
init(state: FxAState?) {
|
||||
self.state = state
|
||||
}
|
||||
public var description: String {
|
||||
return "Not in a Token State: \(state?.label.rawValue ?? "Empty State")"
|
||||
}
|
||||
}
|
||||
|
||||
public class FxAProfile {
|
||||
open var displayName: String?
|
||||
open let email: String
|
||||
open let avatar: Avatar
|
||||
|
||||
init(email: String, displayName: String?, avatar: String?) {
|
||||
self.email = email
|
||||
self.displayName = displayName
|
||||
self.avatar = Avatar(url: avatar?.asURL)
|
||||
}
|
||||
|
||||
enum ImageDownloadState {
|
||||
case notStarted
|
||||
case started
|
||||
case failedCanRetry
|
||||
case failedCanNotRetry
|
||||
case succeededMalformed
|
||||
case succeeded
|
||||
}
|
||||
|
||||
open class Avatar {
|
||||
open var image: UIImage?
|
||||
open let url: URL?
|
||||
var currentImageState: ImageDownloadState = .notStarted
|
||||
|
||||
init(url: URL?) {
|
||||
self.image = UIImage(named: "placeholder-avatar")
|
||||
self.url = url
|
||||
self.updateAvatarImageState()
|
||||
}
|
||||
|
||||
func updateAvatarImageState() {
|
||||
switch currentImageState {
|
||||
case .notStarted:
|
||||
self.currentImageState = .started
|
||||
self.downloadAvatar()
|
||||
break
|
||||
case .failedCanRetry:
|
||||
self.downloadAvatar()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func downloadAvatar() {
|
||||
SDWebImageManager.shared().loadImage(with: url, options: [.continueInBackground, .lowPriority], progress: nil) { (image, _, error, _, success, _) in
|
||||
if let error = error {
|
||||
if (error as NSError).code == 404 || self.currentImageState == .failedCanRetry {
|
||||
// Image is not found or failed to download a second time
|
||||
self.currentImageState = .failedCanNotRetry
|
||||
} else {
|
||||
// This could have been a transient error, attempt to download the image only once more
|
||||
self.currentImageState = .failedCanRetry
|
||||
self.updateAvatarImageState()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if success == true && image == nil {
|
||||
self.currentImageState = .succeededMalformed
|
||||
return
|
||||
}
|
||||
|
||||
self.image = image
|
||||
self.currentImageState = .succeeded
|
||||
NotificationCenter.default.post(name: NotificationFirefoxAccountProfileChanged, object: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch current user's FxA profile. It contains the most updated email, displayName and avatar. This
|
||||
// emits two `NotificationFirefoxAccountProfileChanged`, once when the profile has been downloaded and
|
||||
// another when the avatar image has been downloaded.
|
||||
open func updateProfile() {
|
||||
guard let session = stateCache.value as? TokenState else {
|
||||
return
|
||||
}
|
||||
|
||||
let client = FxAClient10(authEndpoint: self.configuration.authEndpointURL, oauthEndpoint: self.configuration.oauthEndpointURL, profileEndpoint: self.configuration.profileEndpointURL)
|
||||
client.getProfile(withSessionToken: session.sessionToken as NSData) >>== { result in
|
||||
self.fxaProfile = FxAProfile(email: result.email, displayName: result.displayName, avatar: result.avatarURL)
|
||||
NotificationCenter.default.post(name: NotificationFirefoxAccountProfileChanged, object: self)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the devices list from FxA then replace the current stored remote devices.
|
||||
open func updateFxADevices(remoteDevices: RemoteDevices) -> Success {
|
||||
guard let session = stateCache.value as? TokenState else {
|
||||
return deferMaybe(NotATokenStateError(state: stateCache.value))
|
||||
}
|
||||
let client = FxAClient10(authEndpoint: self.configuration.authEndpointURL)
|
||||
return client.devices(withSessionToken: session.sessionToken as NSData) >>== { resp in
|
||||
return remoteDevices.replaceRemoteDevices(resp.devices)
|
||||
}
|
||||
}
|
||||
|
||||
public class NotifyError: MaybeErrorType {
|
||||
public var description = "The server could not notify the clients."
|
||||
}
|
||||
|
||||
@discardableResult open func notify(deviceIDs: [GUID], collectionsChanged collections: [String], reason: String) -> Success {
|
||||
guard let session = stateCache.value as? TokenState else {
|
||||
return deferMaybe(NotATokenStateError(state: stateCache.value))
|
||||
}
|
||||
let client = FxAClient10(authEndpoint: self.configuration.authEndpointURL)
|
||||
return client.notify(deviceIDs: deviceIDs, collectionsChanged: collections, reason: reason, withSessionToken: session.sessionToken as NSData) >>== { resp in
|
||||
guard resp.success else {
|
||||
return deferMaybe(NotifyError())
|
||||
}
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult open func notifyAll(collectionsChanged collections: [String], reason: String) -> Success {
|
||||
guard let session = stateCache.value as? TokenState else {
|
||||
return deferMaybe(NotATokenStateError(state: stateCache.value))
|
||||
}
|
||||
guard let ownDeviceId = self.deviceRegistration?.id else {
|
||||
return deferMaybe(FxAClientError.local(NSError()))
|
||||
}
|
||||
let client = FxAClient10(authEndpoint: self.configuration.authEndpointURL)
|
||||
return client.notifyAll(ownDeviceId: ownDeviceId, collectionsChanged: collections, reason: reason, withSessionToken: session.sessionToken as NSData) >>== { resp in
|
||||
guard resp.success else {
|
||||
return deferMaybe(NotifyError())
|
||||
}
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult open func destroyDevice() -> Success {
|
||||
guard let session = stateCache.value as? TokenState else {
|
||||
return deferMaybe(NotATokenStateError(state: stateCache.value))
|
||||
}
|
||||
guard let ownDeviceId = self.deviceRegistration?.id else {
|
||||
return deferMaybe(FxAClientError.local(NSError()))
|
||||
}
|
||||
let client = FxAClient10(authEndpoint: self.configuration.authEndpointURL)
|
||||
|
||||
return client.destroyDevice(ownDeviceId: ownDeviceId, withSessionToken: session.sessionToken as NSData) >>> succeed
|
||||
}
|
||||
|
||||
@discardableResult open func advance() -> Deferred<FxAState> {
|
||||
OSSpinLockLock(&advanceLock)
|
||||
if let deferred = advanceDeferred {
|
||||
// We already have an advance() in progress. This consumer can chain from it.
|
||||
log.debug("advance already in progress; returning shared deferred.")
|
||||
OSSpinLockUnlock(&advanceLock)
|
||||
return deferred
|
||||
}
|
||||
|
||||
// Alright, we haven't an advance() in progress. Schedule a new deferred to chain from.
|
||||
let cachedState = stateCache.value!
|
||||
var registration = succeed()
|
||||
if let session = cachedState as? TokenState {
|
||||
registration = FxADeviceRegistrator.registerOrUpdateDevice(self, sessionToken: session.sessionToken as NSData).bind { result in
|
||||
if result.successValue != FxADeviceRegistrationResult.alreadyRegistered {
|
||||
NotificationCenter.default.post(name: NotificationFirefoxAccountDeviceRegistrationUpdated, object: nil)
|
||||
}
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
|
||||
let deferred: Deferred<FxAState> = registration.bind { _ in
|
||||
let client = FxAClient10(authEndpoint: self.configuration.authEndpointURL, oauthEndpoint: self.configuration.oauthEndpointURL, profileEndpoint: self.configuration.profileEndpointURL)
|
||||
let stateMachine = FxALoginStateMachine(client: client)
|
||||
let now = Date.now()
|
||||
return stateMachine.advance(fromState: cachedState, now: now).map { newState in
|
||||
self.stateCache.value = newState
|
||||
return newState
|
||||
}
|
||||
}
|
||||
|
||||
advanceDeferred = deferred
|
||||
log.debug("no advance() in progress; setting and returning new shared deferred.")
|
||||
OSSpinLockUnlock(&advanceLock)
|
||||
|
||||
deferred.upon { _ in
|
||||
// This advance() is complete. Clear the shared deferred.
|
||||
OSSpinLockLock(&self.advanceLock)
|
||||
if let existingDeferred = self.advanceDeferred, existingDeferred === deferred {
|
||||
// The guard should not be needed, but should prevent trampling racing consumers.
|
||||
self.advanceDeferred = nil
|
||||
log.debug("advance() completed and shared deferred is existing deferred; clearing shared deferred.")
|
||||
} else {
|
||||
log.warning("advance() completed but shared deferred is not existing deferred; ignoring potential bug!")
|
||||
}
|
||||
OSSpinLockUnlock(&self.advanceLock)
|
||||
}
|
||||
return deferred
|
||||
}
|
||||
|
||||
open func marriedState() -> Deferred<Maybe<MarriedState>> {
|
||||
return advance().map { newState in
|
||||
if newState.label == FxAStateLabel.married {
|
||||
if let married = newState as? MarriedState {
|
||||
return Maybe(success: married)
|
||||
}
|
||||
}
|
||||
return Maybe(failure: AccountError.notMarried)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult open func makeSeparated() -> Bool {
|
||||
log.info("Making Account State be Separated.")
|
||||
self.stateCache.value = SeparatedState()
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult open func makeDoghouse() -> Bool {
|
||||
log.info("Making Account State be Doghouse.")
|
||||
self.stateCache.value = DoghouseState()
|
||||
return true
|
||||
}
|
||||
|
||||
open func makeCohabitingWithoutKeyPair() -> Bool {
|
||||
if let married = self.stateCache.value as? MarriedState {
|
||||
log.info("Making Account State be CohabitingWithoutKeyPair.")
|
||||
self.stateCache.value = married.withoutKeyPair()
|
||||
return true
|
||||
}
|
||||
|
||||
log.info("Cannot make Account State be CohabitingWithoutKeyPair from state with label \(self.stateCache.value?.label ??? "nil").")
|
||||
return false
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue